v4.0.0-beta.456 (#7673)
This commit is contained in:
commit
b6d0d303c8
124 changed files with 2688 additions and 716 deletions
36
CLAUDE.md
36
CLAUDE.md
|
|
@ -10,6 +10,42 @@ ## Project Overview
|
|||
|
||||
Coolify is an open-source, self-hostable platform for deploying applications and managing servers - an alternative to Heroku/Netlify/Vercel. It's built with Laravel (PHP) and uses Docker for containerization.
|
||||
|
||||
## Git Worktree Shared Dependencies
|
||||
|
||||
This repository uses git worktrees for parallel development with **automatic shared dependency setup** via Conductor.
|
||||
|
||||
### How It Works
|
||||
|
||||
The `conductor.json` setup script (`scripts/conductor-setup.sh`) automatically:
|
||||
1. Creates symlinks from worktree's `node_modules` and `vendor` to the main repository's directories
|
||||
2. All worktrees share the same dependencies from the main repository
|
||||
3. This happens automatically when Conductor creates a new worktree
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Save disk space**: Only one copy of dependencies across all worktrees
|
||||
- **Faster setup**: No need to run `npm install` or `composer install` for each worktree
|
||||
- **Consistent versions**: All worktrees use the same dependency versions
|
||||
- **Auto-configured**: Handled by Conductor's setup script
|
||||
- **Simple**: Uses the main repo's existing directories, no extra folders
|
||||
|
||||
### Manual Setup (If Needed)
|
||||
|
||||
If you need to set up symlinks manually or for non-Conductor worktrees:
|
||||
|
||||
```bash
|
||||
# From the worktree directory
|
||||
rm -rf node_modules vendor
|
||||
ln -sf ../../node_modules node_modules
|
||||
ln -sf ../../vendor vendor
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- Dependencies are shared from the main repository (`$CONDUCTOR_ROOT_PATH`)
|
||||
- Run `npm install` or `composer install` from the main repo or any worktree to update all
|
||||
- If different branches need different dependency versions, this won't work - remove symlinks and use separate directories
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Frontend Development
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public function handle(StandaloneClickhouse $database)
|
|||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
|
||||
'test' => "clickhouse-client --user {$this->database->clickhouse_admin_user} --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
|
|
@ -152,12 +152,16 @@ private function generate_environment_variables()
|
|||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
|
||||
$environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}");
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_USER'))->isEmpty()) {
|
||||
$environment_variables->push("CLICKHOUSE_USER={$this->database->clickhouse_admin_user}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("CLICKHOUSE_PASSWORD={$this->database->clickhouse_admin_password}");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_DB'))->isEmpty()) {
|
||||
$environment_variables->push("CLICKHOUSE_DB={$this->database->clickhouse_db}");
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
|
|
|
|||
|
|
@ -199,12 +199,26 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
|||
$isPublic = data_get($database, 'is_public');
|
||||
$foundDatabases[] = $database->id;
|
||||
$statusFromDb = $database->status;
|
||||
|
||||
// Track restart count for databases (single-container)
|
||||
$restartCount = data_get($container, 'RestartCount', 0);
|
||||
$previousRestartCount = $database->restart_count ?? 0;
|
||||
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$database->update(['status' => $containerStatus]);
|
||||
$updateData = ['status' => $containerStatus];
|
||||
} else {
|
||||
$database->update(['last_online_at' => now()]);
|
||||
$updateData = ['last_online_at' => now()];
|
||||
}
|
||||
|
||||
// Update restart tracking if restart count increased
|
||||
if ($restartCount > $previousRestartCount) {
|
||||
$updateData['restart_count'] = $restartCount;
|
||||
$updateData['last_restart_at'] = now();
|
||||
$updateData['last_restart_type'] = 'crash';
|
||||
}
|
||||
|
||||
$database->update($updateData);
|
||||
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->server->isSwarm()) {
|
||||
|
|
@ -365,7 +379,13 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
|
|||
if (str($database->status)->startsWith('exited')) {
|
||||
continue;
|
||||
}
|
||||
$database->update(['status' => 'exited']);
|
||||
// Reset restart tracking when database exits completely
|
||||
$database->update([
|
||||
'status' => 'exited',
|
||||
'restart_count' => 0,
|
||||
'last_restart_at' => null,
|
||||
'last_restart_type' => null,
|
||||
]);
|
||||
|
||||
$name = data_get($database, 'name');
|
||||
$fqdn = data_get($database, 'fqdn');
|
||||
|
|
|
|||
|
|
@ -237,8 +237,9 @@ public function handle()
|
|||
$this->foundProxy = true;
|
||||
} elseif ($type === 'service' && $this->isRunning($containerStatus)) {
|
||||
} else {
|
||||
if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
|
||||
if ($this->allDatabaseUuids->contains($uuid) && $this->isActiveOrTransient($containerStatus)) {
|
||||
$this->foundDatabaseUuids->push($uuid);
|
||||
// TCP proxy should only be started/managed when database is actually running
|
||||
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
|
||||
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
|
||||
} else {
|
||||
|
|
@ -503,20 +504,28 @@ private function updateDatabaseStatus(string $databaseUuid, string $containerSta
|
|||
private function updateNotFoundDatabaseStatus()
|
||||
{
|
||||
$notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
|
||||
if ($notFoundDatabaseUuids->isNotEmpty()) {
|
||||
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
||||
$database = $this->databases->where('uuid', $databaseUuid)->first();
|
||||
if ($database) {
|
||||
if ($database->status !== 'exited') {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
}
|
||||
if ($database->is_public) {
|
||||
StopDatabaseProxy::dispatch($database);
|
||||
}
|
||||
}
|
||||
});
|
||||
if ($notFoundDatabaseUuids->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only protection: Verify we received any container data at all
|
||||
// If containers collection is completely empty, Sentinel might have failed
|
||||
if ($this->containers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
||||
$database = $this->databases->where('uuid', $databaseUuid)->first();
|
||||
if ($database) {
|
||||
if (! str($database->status)->startsWith('exited')) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
}
|
||||
if ($database->is_public) {
|
||||
StopDatabaseProxy::dispatch($database);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus)
|
||||
|
|
@ -576,6 +585,23 @@ private function isRunning(string $containerStatus)
|
|||
return str($containerStatus)->contains('running');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if container is in an active or transient state.
|
||||
* Active states: running
|
||||
* Transient states: restarting, starting, created, paused
|
||||
*
|
||||
* These states indicate the container exists and should be tracked.
|
||||
* Terminal states (exited, dead, removing) should NOT be tracked.
|
||||
*/
|
||||
private function isActiveOrTransient(string $containerStatus): bool
|
||||
{
|
||||
return str($containerStatus)->contains('running') ||
|
||||
str($containerStatus)->contains('restarting') ||
|
||||
str($containerStatus)->contains('starting') ||
|
||||
str($containerStatus)->contains('created') ||
|
||||
str($containerStatus)->contains('paused');
|
||||
}
|
||||
|
||||
private function checkLogDrainContainer()
|
||||
{
|
||||
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
|
||||
|
|
|
|||
|
|
@ -1314,11 +1314,6 @@ private function completeResourceCreation()
|
|||
'server_id' => $this->selectedServerId,
|
||||
];
|
||||
|
||||
// PostgreSQL requires a database_image parameter
|
||||
if ($this->selectedResourceType === 'postgresql') {
|
||||
$queryParams['database_image'] = 'postgres:16-alpine';
|
||||
}
|
||||
|
||||
$this->redirect(route('project.resource.create', [
|
||||
'project_uuid' => $this->selectedProjectUuid,
|
||||
'environment_uuid' => $this->selectedEnvironmentUuid,
|
||||
|
|
|
|||
|
|
@ -117,6 +117,19 @@ public function getLogLinesProperty()
|
|||
});
|
||||
}
|
||||
|
||||
public function copyLogs(): string
|
||||
{
|
||||
$logs = decode_remote_command_output($this->application_deployment_queue)
|
||||
->map(function ($line) {
|
||||
return $line['timestamp'].' '.
|
||||
(isset($line['command']) && $line['command'] ? '[CMD]: ' : '').
|
||||
trim($line['line']);
|
||||
})
|
||||
->join("\n");
|
||||
|
||||
return sanitizeLogsForExport($logs);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.application.deployment.show');
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ class Select extends Component
|
|||
|
||||
protected $queryString = [
|
||||
'server_id',
|
||||
'type' => ['except' => ''],
|
||||
'destination_uuid' => ['except' => '', 'as' => 'destination'],
|
||||
];
|
||||
|
||||
public function mount()
|
||||
|
|
@ -66,6 +68,20 @@ public function mount()
|
|||
$project = Project::whereUuid($projectUuid)->firstOrFail();
|
||||
$this->environments = $project->environments;
|
||||
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
|
||||
|
||||
// Check if we have all required params for PostgreSQL type selection
|
||||
// This handles navigation from global search
|
||||
$queryType = request()->query('type');
|
||||
$queryServerId = request()->query('server_id');
|
||||
$queryDestination = request()->query('destination');
|
||||
|
||||
if ($queryType === 'postgresql' && $queryServerId !== null && $queryDestination) {
|
||||
$this->type = $queryType;
|
||||
$this->server_id = $queryServerId;
|
||||
$this->destination_uuid = $queryDestination;
|
||||
$this->server = Server::find($queryServerId);
|
||||
$this->current_step = 'select-postgresql-type';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ public function mount()
|
|||
|
||||
if (in_array($type, DATABASE_TYPES)) {
|
||||
if ($type->value() === 'postgresql') {
|
||||
// PostgreSQL requires database_image to be explicitly set
|
||||
// If not provided, fall through to Select component for version selection
|
||||
if (! $database_image) {
|
||||
$this->type = $type->value();
|
||||
|
||||
return;
|
||||
}
|
||||
$database = create_standalone_postgresql(
|
||||
environmentId: $environment->id,
|
||||
destinationUuid: $destination_uuid,
|
||||
|
|
|
|||
|
|
@ -67,11 +67,6 @@ public function mount()
|
|||
}
|
||||
}
|
||||
|
||||
public function doSomethingWithThisChunkOfOutput($output)
|
||||
{
|
||||
$this->outputs .= removeAnsiColors($output);
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
|
|
@ -162,23 +157,32 @@ public function getLogs($refresh = false)
|
|||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
}
|
||||
}
|
||||
if ($refresh) {
|
||||
$this->outputs = '';
|
||||
}
|
||||
Process::run($sshCommand, function (string $type, string $output) {
|
||||
$this->doSomethingWithThisChunkOfOutput($output);
|
||||
// Collect new logs into temporary variable first to prevent flickering
|
||||
// (avoids clearing output before new data is ready)
|
||||
$newOutputs = '';
|
||||
Process::run($sshCommand, function (string $type, string $output) use (&$newOutputs) {
|
||||
$newOutputs .= removeAnsiColors($output);
|
||||
});
|
||||
|
||||
if ($this->showTimeStamps) {
|
||||
$this->outputs = str($this->outputs)->split('/\n/')->sort(function ($a, $b) {
|
||||
$newOutputs = str($newOutputs)->split('/\n/')->sort(function ($a, $b) {
|
||||
$a = explode(' ', $a);
|
||||
$b = explode(' ', $b);
|
||||
|
||||
return $a[0] <=> $b[0];
|
||||
})->join("\n");
|
||||
}
|
||||
|
||||
// Only update outputs after new data is ready (atomic update prevents flicker)
|
||||
$this->outputs = $newOutputs;
|
||||
}
|
||||
}
|
||||
|
||||
public function copyLogs(): string
|
||||
{
|
||||
return sanitizeLogsForExport($this->outputs);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.get-logs');
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class Resources extends Component
|
||||
|
|
@ -15,7 +14,7 @@ class Resources extends Component
|
|||
|
||||
public $parameters = [];
|
||||
|
||||
public Collection $containers;
|
||||
public array $unmanagedContainers = [];
|
||||
|
||||
public $activeTab = 'managed';
|
||||
|
||||
|
|
@ -64,7 +63,7 @@ public function loadManagedContainers()
|
|||
{
|
||||
try {
|
||||
$this->activeTab = 'managed';
|
||||
$this->containers = $this->server->refresh()->definedResources();
|
||||
$this->server->refresh();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
@ -74,7 +73,7 @@ public function loadUnmanagedContainers()
|
|||
{
|
||||
$this->activeTab = 'unmanaged';
|
||||
try {
|
||||
$this->containers = $this->server->loadUnmanagedContainers();
|
||||
$this->unmanagedContainers = $this->server->loadUnmanagedContainers()->toArray();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
@ -82,14 +81,12 @@ public function loadUnmanagedContainers()
|
|||
|
||||
public function mount()
|
||||
{
|
||||
$this->containers = collect();
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
$this->loadManagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ class Advanced extends Component
|
|||
#[Validate('boolean')]
|
||||
public bool $disable_two_step_confirmation;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_wire_navigate_enabled;
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
|
|
@ -50,6 +53,7 @@ public function rules()
|
|||
'allowed_ips' => ['nullable', 'string', new ValidIpOrCidr],
|
||||
'is_sponsorship_popup_enabled' => 'boolean',
|
||||
'disable_two_step_confirmation' => 'boolean',
|
||||
'is_wire_navigate_enabled' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +72,7 @@ public function mount()
|
|||
$this->is_api_enabled = $this->settings->is_api_enabled;
|
||||
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
|
||||
$this->is_sponsorship_popup_enabled = $this->settings->is_sponsorship_popup_enabled;
|
||||
$this->is_wire_navigate_enabled = $this->settings->is_wire_navigate_enabled ?? true;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
|
|
@ -146,6 +151,7 @@ public function instantSave()
|
|||
$this->settings->allowed_ips = $this->allowed_ips;
|
||||
$this->settings->is_sponsorship_popup_enabled = $this->is_sponsorship_popup_enabled;
|
||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
|
||||
$this->settings->is_wire_navigate_enabled = $this->is_wire_navigate_enabled;
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'Settings updated!');
|
||||
} catch (\Exception $e) {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@ class Upgrade extends Component
|
|||
|
||||
public string $currentVersion = '';
|
||||
|
||||
public bool $devMode = false;
|
||||
|
||||
protected $listeners = ['updateAvailable' => 'checkUpdate'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
$this->devMode = isDev();
|
||||
}
|
||||
|
||||
public function checkUpdate()
|
||||
|
|
|
|||
|
|
@ -77,21 +77,21 @@ public function generate_preview_fqdn()
|
|||
if ($this->application->fqdn) {
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
|
||||
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
|
||||
} else {
|
||||
$url = Url::fromString($this->application->fqdn);
|
||||
if ($this->fqdn) {
|
||||
$preview_fqdn = getFqdnWithoutPort($this->fqdn);
|
||||
}
|
||||
}
|
||||
$template = $this->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$portInt = $url->getPort();
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$urlPath = $url->getPath();
|
||||
$path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : '';
|
||||
$random = new Cuid2;
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
$preview_fqdn = "$schema://$preview_fqdn{$port}{$path}";
|
||||
$this->fqdn = $preview_fqdn;
|
||||
$this->save();
|
||||
}
|
||||
|
|
@ -147,11 +147,13 @@ public function generate_preview_fqdn_compose()
|
|||
$schema = $url->getScheme();
|
||||
$portInt = $url->getPort();
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$urlPath = $url->getPath();
|
||||
$path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : '';
|
||||
$random = new Cuid2;
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn{$port}";
|
||||
$preview_fqdn = "$schema://$preview_fqdn{$port}{$path}";
|
||||
$preview_domains[] = $preview_fqdn;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class InstanceSettings extends Model
|
|||
'auto_update_frequency' => 'string',
|
||||
'update_check_frequency' => 'string',
|
||||
'sentinel_token' => 'encrypted',
|
||||
'is_wire_navigate_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ class StandaloneClickhouse extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'clickhouse_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
@ -25,7 +28,7 @@ protected static function booted()
|
|||
static::created(function ($database) {
|
||||
LocalPersistentVolume::create([
|
||||
'name' => 'clickhouse-data-'.$database->uuid,
|
||||
'mount_path' => '/bitnami/clickhouse',
|
||||
'mount_path' => '/var/lib/clickhouse',
|
||||
'host_path' => null,
|
||||
'resource_id' => $database->id,
|
||||
'resource_type' => $database->getMorphClass(),
|
||||
|
|
@ -246,8 +249,9 @@ protected function internalDbUrl(): Attribute
|
|||
get: function () {
|
||||
$encodedUser = rawurlencode($this->clickhouse_admin_user);
|
||||
$encodedPass = rawurlencode($this->clickhouse_admin_password);
|
||||
$database = $this->clickhouse_db ?? 'default';
|
||||
|
||||
return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$this->clickhouse_db}";
|
||||
return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->uuid}:9000/{$database}";
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -263,8 +267,9 @@ protected function externalDbUrl(): Attribute
|
|||
}
|
||||
$encodedUser = rawurlencode($this->clickhouse_admin_user);
|
||||
$encodedPass = rawurlencode($this->clickhouse_admin_password);
|
||||
$database = $this->clickhouse_db ?? 'default';
|
||||
|
||||
return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->clickhouse_db}";
|
||||
return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$database}";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ class StandaloneDragonfly extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'dragonfly_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ class StandaloneKeydb extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'keydb_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ class StandaloneMariadb extends BaseModel
|
|||
|
||||
protected $casts = [
|
||||
'mariadb_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ class StandaloneMongodb extends BaseModel
|
|||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($database) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ class StandaloneMysql extends BaseModel
|
|||
protected $casts = [
|
||||
'mysql_password' => 'encrypted',
|
||||
'mysql_root_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ class StandalonePostgresql extends BaseModel
|
|||
protected $casts = [
|
||||
'init_scripts' => 'array',
|
||||
'postgres_password' => 'encrypted',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ class StandaloneRedis extends BaseModel
|
|||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($database) {
|
||||
|
|
|
|||
|
|
@ -269,9 +269,41 @@ function remove_iip($text)
|
|||
// Ensure the input is valid UTF-8 before processing
|
||||
$text = sanitize_utf8_text($text);
|
||||
|
||||
// Git access tokens
|
||||
$text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text);
|
||||
|
||||
return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
|
||||
// ANSI color codes
|
||||
$text = preg_replace('/\x1b\[[0-9;]*m/', '', $text);
|
||||
|
||||
// Generic URLs with passwords (covers database URLs, ftp, amqp, ssh, etc.)
|
||||
// (protocol://user:password@host → protocol://user:<REDACTED>@host)
|
||||
$text = preg_replace('/((?:postgres|mysql|mongodb|rediss?|mariadb|ftp|sftp|ssh|amqp|amqps|ldap|ldaps|s3):\/\/[^:]+:)[^@]+(@)/i', '$1'.REDACTED.'$2', $text);
|
||||
|
||||
// Email addresses
|
||||
$text = preg_replace('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', REDACTED, $text);
|
||||
|
||||
// Bearer/JWT tokens
|
||||
$text = preg_replace('/Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/i', 'Bearer '.REDACTED, $text);
|
||||
|
||||
// GitHub tokens (ghp_ = personal, gho_ = OAuth, ghu_ = user-to-server, ghs_ = server-to-server, ghr_ = refresh)
|
||||
$text = preg_replace('/\b(gh[pousr]_[A-Za-z0-9_]{36,})\b/', REDACTED, $text);
|
||||
|
||||
// GitLab tokens (glpat- = personal access token, glcbt- = CI build token, glrt- = runner token)
|
||||
$text = preg_replace('/\b(gl(?:pat|cbt|rt)-[A-Za-z0-9\-_]{20,})\b/', REDACTED, $text);
|
||||
|
||||
// AWS credentials (Access Key ID starts with AKIA, ABIA, ACCA, ASIA)
|
||||
$text = preg_replace('/\b(A(?:KIA|BIA|CCA|SIA)[A-Z0-9]{16})\b/', REDACTED, $text);
|
||||
|
||||
// AWS Secret Access Key (40 character base64-ish string, typically follows access key)
|
||||
$text = preg_replace('/(aws_secret_access_key|AWS_SECRET_ACCESS_KEY)[=:]\s*[\'"]?([A-Za-z0-9\/+=]{40})[\'"]?/i', '$1='.REDACTED, $text);
|
||||
|
||||
// API keys (common patterns)
|
||||
$text = preg_replace('/(api[_-]?key|apikey|api[_-]?secret|secret[_-]?key)[=:]\s*[\'"]?[A-Za-z0-9\-_]{16,}[\'"]?/i', '$1='.REDACTED, $text);
|
||||
|
||||
// Private key blocks
|
||||
$text = preg_replace('/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/', REDACTED, $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -672,6 +672,12 @@ function removeAnsiColors($text)
|
|||
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
|
||||
}
|
||||
|
||||
function sanitizeLogsForExport(string $text): string
|
||||
{
|
||||
// All sanitization is now handled by remove_iip()
|
||||
return remove_iip($text);
|
||||
}
|
||||
|
||||
function getTopLevelNetworks(Service|Application $resource)
|
||||
{
|
||||
if ($resource->getMorphClass() === \App\Models\Service::class) {
|
||||
|
|
@ -2916,6 +2922,18 @@ function instanceSettings()
|
|||
return InstanceSettings::get();
|
||||
}
|
||||
|
||||
function wireNavigate(): string
|
||||
{
|
||||
try {
|
||||
$settings = instanceSettings();
|
||||
|
||||
// Return wire:navigate.hover for SPA navigation with prefetching, or empty string if disabled
|
||||
return ($settings->is_wire_navigate_enabled ?? true) ? 'wire:navigate.hover' : '';
|
||||
} catch (\Exception $e) {
|
||||
return 'wire:navigate.hover';
|
||||
}
|
||||
}
|
||||
|
||||
function getHelperVersion(): string
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.455',
|
||||
'version' => '4.0.0-beta.456',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* Migrates existing ClickHouse instances from Bitnami/BinamiLegacy images
|
||||
* to the official clickhouse/clickhouse-server image.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Add clickhouse_db column if it doesn't exist
|
||||
if (! Schema::hasColumn('standalone_clickhouses', 'clickhouse_db')) {
|
||||
Schema::table('standalone_clickhouses', function (Blueprint $table) {
|
||||
$table->string('clickhouse_db')
|
||||
->default('default')
|
||||
->after('clickhouse_admin_password');
|
||||
});
|
||||
}
|
||||
|
||||
// Change the default value for the 'image' column to the official image
|
||||
Schema::table('standalone_clickhouses', function (Blueprint $table) {
|
||||
$table->string('image')->default('clickhouse/clickhouse-server:25.11')->change();
|
||||
});
|
||||
|
||||
// Update existing ClickHouse instances from Bitnami images to official image
|
||||
StandaloneClickhouse::where(function ($query) {
|
||||
$query->where('image', 'like', '%bitnami/clickhouse%')
|
||||
->orWhere('image', 'like', '%bitnamilegacy/clickhouse%');
|
||||
})
|
||||
->update([
|
||||
'image' => 'clickhouse/clickhouse-server:25.11',
|
||||
'clickhouse_db' => DB::raw("COALESCE(clickhouse_db, 'default')"),
|
||||
]);
|
||||
|
||||
// Update volume mount paths from Bitnami to official image paths
|
||||
LocalPersistentVolume::where('resource_type', StandaloneClickhouse::class)
|
||||
->where('mount_path', '/bitnami/clickhouse')
|
||||
->update(['mount_path' => '/var/lib/clickhouse']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Revert the default value for the 'image' column
|
||||
Schema::table('standalone_clickhouses', function (Blueprint $table) {
|
||||
$table->string('image')->default('bitnamilegacy/clickhouse')->change();
|
||||
});
|
||||
|
||||
// Revert existing ClickHouse instances back to Bitnami image
|
||||
StandaloneClickhouse::where('image', 'clickhouse/clickhouse-server:25.11')
|
||||
->update(['image' => 'bitnamilegacy/clickhouse']);
|
||||
|
||||
// Revert volume mount paths
|
||||
LocalPersistentVolume::where('resource_type', StandaloneClickhouse::class)
|
||||
->where('mount_path', '/var/lib/clickhouse')
|
||||
->update(['mount_path' => '/bitnami/clickhouse']);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?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::table('instance_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_wire_navigate_enabled')->default(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('is_wire_navigate_enabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* The standalone database tables to add restart tracking columns to.
|
||||
*/
|
||||
private array $tables = [
|
||||
'standalone_postgresqls',
|
||||
'standalone_mysqls',
|
||||
'standalone_mariadbs',
|
||||
'standalone_redis',
|
||||
'standalone_mongodbs',
|
||||
'standalone_keydbs',
|
||||
'standalone_dragonflies',
|
||||
'standalone_clickhouses',
|
||||
];
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
if (! Schema::hasColumn($table, 'restart_count')) {
|
||||
Schema::table($table, function (Blueprint $blueprint) {
|
||||
$blueprint->integer('restart_count')->default(0)->after('status');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn($table, 'last_restart_at')) {
|
||||
Schema::table($table, function (Blueprint $blueprint) {
|
||||
$blueprint->timestamp('last_restart_at')->nullable()->after('restart_count');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn($table, 'last_restart_type')) {
|
||||
Schema::table($table, function (Blueprint $blueprint) {
|
||||
$blueprint->string('last_restart_type', 10)->nullable()->after('last_restart_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$columns = ['restart_count', 'last_restart_at', 'last_restart_type'];
|
||||
|
||||
foreach ($this->tables as $table) {
|
||||
foreach ($columns as $column) {
|
||||
if (Schema::hasColumn($table, $column)) {
|
||||
Schema::table($table, function (Blueprint $blueprint) use ($column) {
|
||||
$blueprint->dropColumn($column);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -29,9 +29,14 @@ if [ $EUID != 0 ]; then
|
|||
exit
|
||||
fi
|
||||
|
||||
echo -e "Welcome to Coolify Installer!"
|
||||
echo -e "This script will install everything for you. Sit back and relax."
|
||||
echo -e "Source code: https://github.com/coollabsio/coolify/blob/v4.x/scripts/install.sh"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Coolify Installation - ${DATE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Welcome to Coolify Installer!"
|
||||
echo "This script will install everything for you. Sit back and relax."
|
||||
echo "Source code: https://github.com/coollabsio/coolify/blob/v4.x/scripts/install.sh"
|
||||
|
||||
# Predefined root user
|
||||
ROOT_USERNAME=${ROOT_USERNAME:-}
|
||||
|
|
@ -242,6 +247,29 @@ getAJoke() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Helper function to log with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Helper function to log section headers
|
||||
log_section() {
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
echo "============================================================"
|
||||
}
|
||||
|
||||
# Helper function to check if all required packages are installed
|
||||
all_packages_installed() {
|
||||
for pkg in curl wget git jq openssl; do
|
||||
if ! command -v "$pkg" >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
|
|
@ -288,9 +316,11 @@ if [ "$OS_TYPE" = 'amzn' ]; then
|
|||
dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
LATEST_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $2}' | tr -d ',')
|
||||
LATEST_HELPER_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $6}' | tr -d ',')
|
||||
LATEST_REALTIME_VERSION=$(curl -L --silent $CDN/versions.json | grep -i version | xargs | awk '{print $8}' | tr -d ',')
|
||||
# Fetch versions.json once and parse all values from it
|
||||
VERSIONS_JSON=$(curl -L --silent $CDN/versions.json)
|
||||
LATEST_VERSION=$(echo "$VERSIONS_JSON" | grep -i version | xargs | awk '{print $2}' | tr -d ',')
|
||||
LATEST_HELPER_VERSION=$(echo "$VERSIONS_JSON" | grep -i version | xargs | awk '{print $6}' | tr -d ',')
|
||||
LATEST_REALTIME_VERSION=$(echo "$VERSIONS_JSON" | grep -i version | xargs | awk '{print $8}' | tr -d ',')
|
||||
|
||||
if [ -z "$LATEST_HELPER_VERSION" ]; then
|
||||
LATEST_HELPER_VERSION=latest
|
||||
|
|
@ -315,7 +345,7 @@ if [ "$1" != "" ]; then
|
|||
LATEST_VERSION="${LATEST_VERSION#v}"
|
||||
fi
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo "| Coolify | $LATEST_VERSION"
|
||||
|
|
@ -323,46 +353,61 @@ echo "| Helper | $LATEST_HELPER_VERSION"
|
|||
echo "| Realtime | $LATEST_REALTIME_VERSION"
|
||||
echo "| Docker Pool | $DOCKER_ADDRESS_POOL_BASE (size $DOCKER_ADDRESS_POOL_SIZE)"
|
||||
echo "| Registry URL | $REGISTRY_URL"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
echo "---------------------------------------------"
|
||||
echo ""
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
log_section "Step 1/9: Installing required packages"
|
||||
echo "1/9 Installing required packages (curl, wget, git, jq, openssl)..."
|
||||
|
||||
echo -e "2. Check OpenSSH server configuration. "
|
||||
# Track if apt-get update was run to avoid redundant calls later
|
||||
APT_UPDATED=false
|
||||
|
||||
if all_packages_installed; then
|
||||
log "All required packages already installed, skipping installation"
|
||||
echo " - All required packages already installed."
|
||||
else
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
APT_UPDATED=true
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git jq openssl >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
log "Required packages installed successfully"
|
||||
fi
|
||||
echo " Done."
|
||||
|
||||
log_section "Step 2/9: Checking OpenSSH server configuration"
|
||||
echo "2/9 Checking OpenSSH server configuration..."
|
||||
|
||||
# Detect OpenSSH server
|
||||
SSH_DETECTED=false
|
||||
|
|
@ -398,7 +443,10 @@ if [ "$SSH_DETECTED" = "false" ]; then
|
|||
service sshd start >/dev/null 2>&1
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
if [ "$APT_UPDATED" = false ]; then
|
||||
apt-get update -y >/dev/null
|
||||
APT_UPDATED=true
|
||||
fi
|
||||
apt-get install -y openssh-server >/dev/null
|
||||
systemctl enable ssh >/dev/null 2>&1
|
||||
systemctl start ssh >/dev/null 2>&1
|
||||
|
|
@ -465,7 +513,10 @@ install_docker() {
|
|||
install_docker_manually() {
|
||||
case "$OS_TYPE" in
|
||||
"ubuntu" | "debian" | "raspbian")
|
||||
apt-get update
|
||||
if [ "$APT_UPDATED" = false ]; then
|
||||
apt-get update
|
||||
APT_UPDATED=true
|
||||
fi
|
||||
apt-get install -y ca-certificates curl
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/$OS_TYPE/gpg -o /etc/apt/keyrings/docker.asc
|
||||
|
|
@ -491,7 +542,8 @@ install_docker_manually() {
|
|||
echo "Docker installed successfully."
|
||||
fi
|
||||
}
|
||||
echo -e "3. Check Docker Installation. "
|
||||
log_section "Step 3/9: Checking Docker installation"
|
||||
echo "3/9 Checking Docker installation..."
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
getAJoke
|
||||
|
|
@ -575,7 +627,8 @@ else
|
|||
echo " - Docker is installed."
|
||||
fi
|
||||
|
||||
echo -e "4. Check Docker Configuration. "
|
||||
log_section "Step 4/9: Checking Docker configuration"
|
||||
echo "4/9 Checking Docker configuration..."
|
||||
|
||||
echo " - Network pool configuration: ${DOCKER_ADDRESS_POOL_BASE}/${DOCKER_ADDRESS_POOL_SIZE}"
|
||||
echo " - To override existing configuration: DOCKER_POOL_FORCE_OVERRIDE=true"
|
||||
|
|
@ -704,13 +757,38 @@ else
|
|||
fi
|
||||
fi
|
||||
|
||||
echo -e "5. Download required files from CDN. "
|
||||
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
curl -fsSL -L $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh
|
||||
log_section "Step 5/9: Downloading required files from CDN"
|
||||
echo "5/9 Downloading required files from CDN..."
|
||||
log "Downloading configuration files in parallel..."
|
||||
|
||||
echo -e "6. Setting up environment variable file"
|
||||
# Download files in parallel for faster installation
|
||||
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml &
|
||||
PID1=$!
|
||||
curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml &
|
||||
PID2=$!
|
||||
curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production &
|
||||
PID3=$!
|
||||
curl -fsSL -L $CDN/upgrade.sh -o /data/coolify/source/upgrade.sh &
|
||||
PID4=$!
|
||||
|
||||
# Wait for all downloads to complete and check for errors
|
||||
DOWNLOAD_FAILED=false
|
||||
for PID in $PID1 $PID2 $PID3 $PID4; do
|
||||
if ! wait $PID; then
|
||||
DOWNLOAD_FAILED=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$DOWNLOAD_FAILED" = true ]; then
|
||||
echo " - ERROR: One or more downloads failed. Please check your network connection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "All configuration files downloaded successfully"
|
||||
echo " Done."
|
||||
|
||||
log_section "Step 6/9: Setting up environment variable file"
|
||||
echo "6/9 Setting up environment variable file..."
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
# If .env exists, create backup
|
||||
|
|
@ -725,8 +803,11 @@ else
|
|||
echo " - No .env file found, copying .env.production to .env"
|
||||
cp "/data/coolify/source/.env.production" "$ENV_FILE"
|
||||
fi
|
||||
log "Environment file setup completed"
|
||||
echo " Done."
|
||||
|
||||
echo -e "7. Checking and updating environment variables if necessary..."
|
||||
log_section "Step 7/9: Checking and updating environment variables"
|
||||
echo "7/9 Checking and updating environment variables..."
|
||||
|
||||
update_env_var() {
|
||||
local key="$1"
|
||||
|
|
@ -786,8 +867,11 @@ else
|
|||
update_env_var "DOCKER_ADDRESS_POOL_SIZE" "$DOCKER_ADDRESS_POOL_SIZE"
|
||||
fi
|
||||
fi
|
||||
log "Environment variables check completed"
|
||||
echo " Done."
|
||||
|
||||
echo -e "8. Checking for SSH key for localhost access."
|
||||
log_section "Step 8/9: Checking SSH key for localhost access"
|
||||
echo "8/9 Checking SSH key for localhost access..."
|
||||
if [ ! -f ~/.ssh/authorized_keys ]; then
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
|
|
@ -812,8 +896,11 @@ fi
|
|||
|
||||
chown -R 9999:root /data/coolify
|
||||
chmod -R 700 /data/coolify
|
||||
log "SSH key check completed"
|
||||
echo " Done."
|
||||
|
||||
echo -e "9. Installing Coolify ($LATEST_VERSION)"
|
||||
log_section "Step 9/9: Installing Coolify"
|
||||
echo "9/9 Installing Coolify ($LATEST_VERSION)..."
|
||||
echo -e " - It could take a while based on your server's performance, network speed, stars, etc."
|
||||
echo -e " - Please wait."
|
||||
getAJoke
|
||||
|
|
@ -824,11 +911,85 @@ else
|
|||
bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" "${REGISTRY_URL:-ghcr.io}" "true"
|
||||
fi
|
||||
echo " - Coolify installed successfully."
|
||||
echo " - Waiting for Coolify to be ready..."
|
||||
|
||||
echo " - Waiting 20 seconds for Coolify database migrations to complete."
|
||||
getAJoke
|
||||
# Wait for upgrade.sh background process to complete
|
||||
# upgrade.sh writes status to /data/coolify/source/.upgrade-status
|
||||
# Status file format: step|message|timestamp
|
||||
# Step 6 = "Upgrade complete", file deleted 10 seconds after
|
||||
UPGRADE_STATUS_FILE="/data/coolify/source/.upgrade-status"
|
||||
MAX_WAIT=180
|
||||
WAITED=0
|
||||
SEEN_STATUS_FILE=false
|
||||
|
||||
sleep 20
|
||||
while [ $WAITED -lt $MAX_WAIT ]; do
|
||||
if [ -f "$UPGRADE_STATUS_FILE" ]; then
|
||||
SEEN_STATUS_FILE=true
|
||||
STATUS=$(cat "$UPGRADE_STATUS_FILE" 2>/dev/null | cut -d'|' -f1)
|
||||
MESSAGE=$(cat "$UPGRADE_STATUS_FILE" 2>/dev/null | cut -d'|' -f2)
|
||||
if [ "$STATUS" = "6" ]; then
|
||||
log "Upgrade completed: $MESSAGE"
|
||||
echo " - Upgrade complete!"
|
||||
break
|
||||
elif [ "$STATUS" = "error" ]; then
|
||||
echo " - ERROR: Upgrade failed: $MESSAGE"
|
||||
echo " - Please check the upgrade logs: /data/coolify/source/upgrade-*.log"
|
||||
exit 1
|
||||
else
|
||||
if [ $((WAITED % 10)) -eq 0 ]; then
|
||||
echo " - Upgrade in progress: $MESSAGE (${WAITED}s)"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Status file doesn't exist
|
||||
if [ "$SEEN_STATUS_FILE" = true ]; then
|
||||
# We saw the file before, now it's gone = upgrade completed and cleaned up
|
||||
log "Upgrade status file cleaned up - upgrade complete"
|
||||
echo " - Upgrade complete!"
|
||||
break
|
||||
fi
|
||||
# Haven't seen status file yet - either very early or upgrade.sh hasn't started
|
||||
if [ $((WAITED % 10)) -eq 0 ] && [ $WAITED -gt 0 ]; then
|
||||
echo " - Waiting for upgrade process to start... (${WAITED}s)"
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
WAITED=$((WAITED + 2))
|
||||
done
|
||||
|
||||
if [ $WAITED -ge $MAX_WAIT ]; then
|
||||
if [ "$SEEN_STATUS_FILE" = false ]; then
|
||||
# Never saw status file - fallback to old behavior (wait 20s + health check)
|
||||
log "Status file not found, using fallback wait"
|
||||
echo " - Status file not found, waiting 20 seconds..."
|
||||
sleep 20
|
||||
else
|
||||
echo " - ERROR: Upgrade timed out after ${MAX_WAIT}s"
|
||||
echo " - Please check the upgrade logs: /data/coolify/source/upgrade-*.log"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Final health verification - wait for container to be healthy
|
||||
echo " - Verifying Coolify is healthy..."
|
||||
HEALTH_WAIT=60
|
||||
HEALTH_WAITED=0
|
||||
while [ $HEALTH_WAITED -lt $HEALTH_WAIT ]; do
|
||||
HEALTH=$(docker inspect --format='{{.State.Health.Status}}' coolify 2>/dev/null || echo "unknown")
|
||||
if [ "$HEALTH" = "healthy" ]; then
|
||||
log "Coolify container is healthy"
|
||||
echo " - Coolify is ready!"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
HEALTH_WAITED=$((HEALTH_WAITED + 2))
|
||||
done
|
||||
|
||||
if [ "$HEALTH" != "healthy" ]; then
|
||||
echo " - ERROR: Coolify container is not healthy after ${HEALTH_WAIT}s. Status: $HEALTH"
|
||||
echo " - Please check: docker logs coolify"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "\033[0;35m
|
||||
____ _ _ _ _ _
|
||||
/ ___|___ _ __ __ _ _ __ __ _| |_ _ _| | __ _| |_(_) ___ _ __ ___| |
|
||||
|
|
@ -838,8 +999,18 @@ echo -e "\033[0;35m
|
|||
|___/
|
||||
\033[0m"
|
||||
|
||||
IPV4_PUBLIC_IP=$(curl -4s https://ifconfig.io || true)
|
||||
IPV6_PUBLIC_IP=$(curl -6s https://ifconfig.io || true)
|
||||
# Fetch public IPs in parallel for faster completion
|
||||
IPV4_TMP=$(mktemp)
|
||||
IPV6_TMP=$(mktemp)
|
||||
curl -4s --max-time 5 https://ifconfig.io > "$IPV4_TMP" 2>/dev/null &
|
||||
IPV4_PID=$!
|
||||
curl -6s --max-time 5 https://ifconfig.io > "$IPV6_TMP" 2>/dev/null &
|
||||
IPV6_PID=$!
|
||||
wait $IPV4_PID 2>/dev/null || true
|
||||
wait $IPV6_PID 2>/dev/null || true
|
||||
IPV4_PUBLIC_IP=$(cat "$IPV4_TMP" 2>/dev/null || true)
|
||||
IPV6_PUBLIC_IP=$(cat "$IPV6_TMP" 2>/dev/null || true)
|
||||
rm -f "$IPV4_TMP" "$IPV6_TMP"
|
||||
|
||||
echo -e "\nYour instance is ready to use!\n"
|
||||
if [ -n "$IPV4_PUBLIC_IP" ]; then
|
||||
|
|
@ -864,3 +1035,8 @@ if [ -n "$PRIVATE_IPS" ]; then
|
|||
fi
|
||||
|
||||
echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n"
|
||||
|
||||
log_section "Installation Complete"
|
||||
log "Coolify installation completed successfully"
|
||||
log "Version: ${LATEST_VERSION}"
|
||||
log "Log file: ${INSTALLATION_LOG_WITH_DATE}"
|
||||
|
|
|
|||
|
|
@ -7,27 +7,101 @@ LATEST_HELPER_VERSION=${2:-latest}
|
|||
REGISTRY_URL=${3:-ghcr.io}
|
||||
SKIP_BACKUP=${4:-false}
|
||||
ENV_FILE="/data/coolify/source/.env"
|
||||
STATUS_FILE="/data/coolify/source/.upgrade-status"
|
||||
|
||||
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
||||
LOGFILE="/data/coolify/source/upgrade-${DATE}.log"
|
||||
|
||||
curl -fsSL $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
curl -fsSL $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
# Helper function to log with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE"
|
||||
}
|
||||
|
||||
# Helper function to log section headers
|
||||
log_section() {
|
||||
echo "" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
}
|
||||
|
||||
# Helper function to write upgrade status for API polling
|
||||
write_status() {
|
||||
local step="$1"
|
||||
local message="$2"
|
||||
echo "${step}|${message}|$(date -Iseconds)" > "$STATUS_FILE"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Coolify Upgrade - ${DATE}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Initialize log file with header
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
echo "Coolify Upgrade Log" >>"$LOGFILE"
|
||||
echo "Started: $(date '+%Y-%m-%d %H:%M:%S')" >>"$LOGFILE"
|
||||
echo "Target Version: ${LATEST_IMAGE}" >>"$LOGFILE"
|
||||
echo "Helper Version: ${LATEST_HELPER_VERSION}" >>"$LOGFILE"
|
||||
echo "Registry URL: ${REGISTRY_URL}" >>"$LOGFILE"
|
||||
echo "============================================================" >>"$LOGFILE"
|
||||
|
||||
log_section "Step 1/6: Downloading configuration files"
|
||||
write_status "1" "Downloading configuration files"
|
||||
echo "1/6 Downloading latest configuration files..."
|
||||
log "Downloading docker-compose.yml from ${CDN}/docker-compose.yml"
|
||||
curl -fsSL -L $CDN/docker-compose.yml -o /data/coolify/source/docker-compose.yml
|
||||
log "Downloading docker-compose.prod.yml from ${CDN}/docker-compose.prod.yml"
|
||||
curl -fsSL -L $CDN/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
|
||||
log "Downloading .env.production from ${CDN}/.env.production"
|
||||
curl -fsSL -L $CDN/.env.production -o /data/coolify/source/.env.production
|
||||
log "Configuration files downloaded successfully"
|
||||
echo " Done."
|
||||
|
||||
# Extract all images from docker-compose configuration
|
||||
log "Extracting all images from docker-compose configuration..."
|
||||
COMPOSE_FILES="-f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml"
|
||||
|
||||
# Check if custom compose file exists
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
COMPOSE_FILES="$COMPOSE_FILES -f /data/coolify/source/docker-compose.custom.yml"
|
||||
log "Including custom docker-compose.yml in image extraction"
|
||||
fi
|
||||
|
||||
# Get all unique images from docker compose config
|
||||
# LATEST_IMAGE env var is needed for image substitution in compose files
|
||||
IMAGES=$(LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file "$ENV_FILE" $COMPOSE_FILES config --images 2>/dev/null | sort -u)
|
||||
|
||||
if [ -z "$IMAGES" ]; then
|
||||
log "ERROR: Failed to extract images from docker-compose files"
|
||||
write_status "error" "Failed to parse docker-compose configuration"
|
||||
echo " ERROR: Failed to parse docker-compose configuration. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Images to pull:"
|
||||
echo "$IMAGES" | while read img; do log " - $img"; done
|
||||
|
||||
# Backup existing .env file before making any changes
|
||||
if [ "$SKIP_BACKUP" != "true" ]; then
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "Creating backup of existing .env file to .env-$DATE" >>"$LOGFILE"
|
||||
echo " Creating backup of .env file..."
|
||||
log "Creating backup of .env file to .env-$DATE"
|
||||
cp "$ENV_FILE" "$ENV_FILE-$DATE"
|
||||
log "Backup created: ${ENV_FILE}-${DATE}"
|
||||
else
|
||||
echo "No existing .env file found to backup" >>"$LOGFILE"
|
||||
log "WARNING: No existing .env file found to backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Merging .env.production values into .env" >>"$LOGFILE"
|
||||
log_section "Step 2/6: Updating environment configuration"
|
||||
write_status "2" "Updating environment configuration"
|
||||
echo ""
|
||||
echo "2/6 Updating environment configuration..."
|
||||
log "Merging .env.production values into .env"
|
||||
awk -F '=' '!seen[$1]++' "$ENV_FILE" /data/coolify/source/.env.production > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
echo ".env file merged successfully" >>"$LOGFILE"
|
||||
log "Environment file merged successfully"
|
||||
|
||||
update_env_var() {
|
||||
local key="$1"
|
||||
|
|
@ -36,73 +110,173 @@ update_env_var() {
|
|||
# If variable "key=" exists but has no value, update the value of the existing line
|
||||
if grep -q "^${key}=$" "$ENV_FILE"; then
|
||||
sed -i "s|^${key}=$|${key}=${value}|" "$ENV_FILE"
|
||||
echo " - Updated value of ${key} as the current value was empty" >>"$LOGFILE"
|
||||
log "Updated ${key} (was empty)"
|
||||
# If variable "key=" doesn't exist, append it to the file with value
|
||||
elif ! grep -q "^${key}=" "$ENV_FILE"; then
|
||||
printf '%s=%s\n' "$key" "$value" >>"$ENV_FILE"
|
||||
echo " - Added ${key} with default value as the variable was missing" >>"$LOGFILE"
|
||||
log "Added ${key} (was missing)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Checking and updating environment variables if necessary..." >>"$LOGFILE"
|
||||
log "Checking environment variables..."
|
||||
update_env_var "PUSHER_APP_ID" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_KEY" "$(openssl rand -hex 32)"
|
||||
update_env_var "PUSHER_APP_SECRET" "$(openssl rand -hex 32)"
|
||||
log "Environment variables check complete"
|
||||
echo " Done."
|
||||
|
||||
# Make sure coolify network exists
|
||||
# It is created when starting Coolify with docker compose
|
||||
log "Checking Docker network 'coolify'..."
|
||||
if ! docker network inspect coolify >/dev/null 2>&1; then
|
||||
log "Network 'coolify' does not exist, creating..."
|
||||
if ! docker network create --attachable --ipv6 coolify 2>/dev/null; then
|
||||
echo "Failed to create coolify network with ipv6. Trying without ipv6..."
|
||||
log "Failed to create network with IPv6, trying without IPv6..."
|
||||
docker network create --attachable coolify 2>/dev/null
|
||||
log "Network 'coolify' created without IPv6"
|
||||
else
|
||||
log "Network 'coolify' created with IPv6 support"
|
||||
fi
|
||||
else
|
||||
log "Network 'coolify' already exists"
|
||||
fi
|
||||
|
||||
# Check if Docker config file exists
|
||||
DOCKER_CONFIG_MOUNT=""
|
||||
if [ -f /root/.docker/config.json ]; then
|
||||
DOCKER_CONFIG_MOUNT="-v /root/.docker/config.json:/root/.docker/config.json"
|
||||
log "Docker config mount enabled: /root/.docker/config.json"
|
||||
fi
|
||||
|
||||
# Pull all required images before stopping containers
|
||||
# This ensures we don't take down the system if image pull fails (rate limits, network issues, etc.)
|
||||
echo "Pulling required Docker images..." >>"$LOGFILE"
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify:${LATEST_IMAGE}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify helper image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull postgres:15-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull PostgreSQL image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
docker pull redis:7-alpine >>"$LOGFILE" 2>&1 || { echo "Failed to pull Redis image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
# Pull realtime image - version is hardcoded in docker-compose.prod.yml, extract it or use a known version
|
||||
docker pull "${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10" >>"$LOGFILE" 2>&1 || { echo "Failed to pull Coolify realtime image. Aborting upgrade." >>"$LOGFILE"; exit 1; }
|
||||
echo "All images pulled successfully." >>"$LOGFILE"
|
||||
log_section "Step 3/6: Pulling Docker images"
|
||||
write_status "3" "Pulling Docker images"
|
||||
echo ""
|
||||
echo "3/6 Pulling Docker images..."
|
||||
echo " This may take a few minutes depending on your connection."
|
||||
|
||||
# Stop and remove existing Coolify containers to prevent conflicts
|
||||
# This handles both old installations (project "source") and new ones (project "coolify")
|
||||
# Use nohup to ensure the script continues even if SSH connection is lost
|
||||
echo "Starting container restart sequence (detached)..." >>"$LOGFILE"
|
||||
# Also pull the helper image (not in compose files but needed for upgrade)
|
||||
HELPER_IMAGE="${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:${LATEST_HELPER_VERSION}"
|
||||
echo " - Pulling $HELPER_IMAGE..."
|
||||
log "Pulling image: $HELPER_IMAGE"
|
||||
if docker pull "$HELPER_IMAGE" >>"$LOGFILE" 2>&1; then
|
||||
log "Successfully pulled $HELPER_IMAGE"
|
||||
else
|
||||
log "ERROR: Failed to pull $HELPER_IMAGE"
|
||||
write_status "error" "Failed to pull $HELPER_IMAGE"
|
||||
echo " ERROR: Failed to pull $HELPER_IMAGE. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pull all images from compose config
|
||||
# Using a for loop to avoid subshell issues with exit
|
||||
for IMAGE in $IMAGES; do
|
||||
if [ -n "$IMAGE" ]; then
|
||||
echo " - Pulling $IMAGE..."
|
||||
log "Pulling image: $IMAGE"
|
||||
if docker pull "$IMAGE" >>"$LOGFILE" 2>&1; then
|
||||
log "Successfully pulled $IMAGE"
|
||||
else
|
||||
log "ERROR: Failed to pull $IMAGE"
|
||||
write_status "error" "Failed to pull $IMAGE"
|
||||
echo " ERROR: Failed to pull $IMAGE. Aborting upgrade."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log "All images pulled successfully"
|
||||
echo " All images pulled successfully."
|
||||
|
||||
log_section "Step 4/6: Stopping and restarting containers"
|
||||
write_status "4" "Stopping containers"
|
||||
echo ""
|
||||
echo "4/6 Stopping containers and starting new ones..."
|
||||
echo " This step will restart all Coolify containers."
|
||||
echo " Check the log file for details: ${LOGFILE}"
|
||||
|
||||
# From this point forward, we need to ensure the script continues even if
|
||||
# the SSH connection is lost (which happens when coolify container stops)
|
||||
# We use a subshell with nohup to ensure completion
|
||||
log "Starting container restart sequence (detached)..."
|
||||
|
||||
nohup bash -c "
|
||||
LOGFILE='$LOGFILE'
|
||||
STATUS_FILE='$STATUS_FILE'
|
||||
DOCKER_CONFIG_MOUNT='$DOCKER_CONFIG_MOUNT'
|
||||
REGISTRY_URL='$REGISTRY_URL'
|
||||
LATEST_HELPER_VERSION='$LATEST_HELPER_VERSION'
|
||||
LATEST_IMAGE='$LATEST_IMAGE'
|
||||
|
||||
log() {
|
||||
echo \"[\$(date '+%Y-%m-%d %H:%M:%S')] \$1\" >>\"\$LOGFILE\"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
echo \"\$1|\$2|\$(date -Iseconds)\" > \"\$STATUS_FILE\"
|
||||
}
|
||||
|
||||
# Stop and remove containers
|
||||
echo 'Stopping existing Coolify containers...' >>\"\$LOGFILE\"
|
||||
for container in coolify coolify-db coolify-redis coolify-realtime; do
|
||||
if docker ps -a --format '{{.Names}}' | grep -q \"^\${container}\$\"; then
|
||||
log \"Stopping container: \${container}\"
|
||||
docker stop \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
log \"Removing container: \${container}\"
|
||||
docker rm \"\$container\" >>\"\$LOGFILE\" 2>&1 || true
|
||||
echo \" - Removed container: \$container\" >>\"\$LOGFILE\"
|
||||
log \"Container \${container} stopped and removed\"
|
||||
else
|
||||
log \"Container \${container} not found (skipping)\"
|
||||
fi
|
||||
done
|
||||
log \"Container cleanup complete\"
|
||||
|
||||
# Start new containers
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
log 'Step 5/6: Starting new containers'
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
write_status '5' 'Starting new containers'
|
||||
|
||||
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
|
||||
echo 'docker-compose.custom.yml detected.' >>\"\$LOGFILE\"
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
log 'Using custom docker-compose.yml'
|
||||
log 'Running docker compose up with custom configuration...'
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
else
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --project-name coolify --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
log 'Using standard docker-compose configuration'
|
||||
log 'Running docker compose up...'
|
||||
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock \${DOCKER_CONFIG_MOUNT} --rm \${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-helper:\${LATEST_HELPER_VERSION} bash -c \"LATEST_IMAGE=\${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --remove-orphans --wait --wait-timeout 60\" >>\"\$LOGFILE\" 2>&1
|
||||
fi
|
||||
echo 'Upgrade completed.' >>\"\$LOGFILE\"
|
||||
log 'Docker compose up completed'
|
||||
|
||||
# Final log entry
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
log 'Step 6/6: Upgrade complete'
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
write_status '6' 'Upgrade complete'
|
||||
log 'Coolify upgrade completed successfully'
|
||||
log \"Version: \${LATEST_IMAGE}\"
|
||||
echo '' >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
echo \"Upgrade completed: \$(date '+%Y-%m-%d %H:%M:%S')\" >>\"\$LOGFILE\"
|
||||
echo '============================================================' >>\"\$LOGFILE\"
|
||||
|
||||
# Clean up status file after a short delay to allow frontend to read completion
|
||||
sleep 10
|
||||
rm -f \"\$STATUS_FILE\"
|
||||
log 'Status file cleaned up'
|
||||
" >>"$LOGFILE" 2>&1 &
|
||||
|
||||
# Give the background process a moment to start
|
||||
sleep 2
|
||||
log "Container restart sequence started in background (PID: $!)"
|
||||
echo ""
|
||||
echo "5/6 Containers are being restarted in the background..."
|
||||
echo "6/6 Upgrade process initiated!"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Coolify upgrade to ${LATEST_IMAGE} in progress"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " The upgrade will continue in the background."
|
||||
echo " Coolify will be available again shortly."
|
||||
echo " Log file: ${LOGFILE}"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.455"
|
||||
"version": "4.0.0-beta.456"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.456"
|
||||
"version": "4.0.0-beta.457"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.12"
|
||||
|
|
@ -17,13 +17,13 @@
|
|||
}
|
||||
},
|
||||
"traefik": {
|
||||
"v3.6": "3.6.1",
|
||||
"v3.6": "3.6.5",
|
||||
"v3.5": "3.5.6",
|
||||
"v3.4": "3.4.5",
|
||||
"v3.3": "3.3.7",
|
||||
"v3.2": "3.2.5",
|
||||
"v3.1": "3.1.7",
|
||||
"v3.0": "3.0.4",
|
||||
"v2.11": "2.11.31"
|
||||
"v2.11": "2.11.32"
|
||||
}
|
||||
}
|
||||
12
public/svgs/appflowy.svg
Normal file
12
public/svgs/appflowy.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="352" height="351" viewBox="0 0 352 351" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M351.694 211.848C341.593 266.47 297.512 314.204 246.499 341.01C240.117 344.355 237.564 346.269 230.367 346.732H333.912C336.243 346.805 338.566 346.41 340.741 345.57C342.917 344.73 344.902 343.462 346.579 341.84C348.255 340.219 349.589 338.278 350.502 336.132C351.415 333.985 351.888 331.678 351.892 329.346V211.848H351.694Z" fill="#F7931E" />
|
||||
<path d="M238.823 343.613C236.728 343.767 234.625 343.767 232.529 343.613H238.823Z" fill="#FFCE00" />
|
||||
<path d="M135.699 106.437C134.136 107.713 132.574 108.902 130.967 110.024C104.756 128.466 24.7373 188.283 6.62514 162.534C-11.1349 137.269 7.57146 65.591 53.6331 31.1053C54.5134 30.401 55.3937 29.7848 56.296 29.1466C106.319 -5.99935 143.732 -1.1357 161.822 24.6351C178.812 48.7993 159.797 86.6741 135.699 106.437Z" fill="#8427E0" />
|
||||
<path d="M331.277 161.796C306.519 179.182 267.455 158.891 248.111 134.023C247.319 133.011 246.57 131.976 245.91 130.964C227.49 104.731 167.674 24.7118 193.422 6.62164C219.171 -11.4685 293.094 8.29421 326.81 56.2925C327.558 57.3489 328.284 58.4933 329.01 59.4396C361.691 108.01 356.542 144.08 331.277 161.796Z" fill="#00B5FF" />
|
||||
<path d="M298.158 319.655C297.278 320.337 296.42 320.975 295.561 321.569C245.538 356.781 208.126 351.83 190.036 326.103C173.068 301.895 192.038 264.086 216.136 244.301C217.677 243.003 219.284 241.792 220.89 240.67C247.101 222.25 327.12 162.455 345.21 188.182C362.97 213.447 344.264 285.169 298.158 319.655Z" fill="#FFBD00" />
|
||||
<path d="M158.493 344.09C132.745 362.158 58.8216 342.418 25.1062 294.419C24.402 293.451 23.7638 292.483 23.1036 291.514C-9.79756 242.922 -4.6038 206.653 20.6167 188.915C45.3531 171.529 84.4383 191.798 103.761 216.689C104.553 217.701 105.301 218.714 105.962 219.748C124.47 245.981 184.264 326 158.493 344.09Z" fill="#E3006D" />
|
||||
<path d="M135.695 106.437C99.9326 122.15 24.0948 154.853 12.695 128.642C3.14372 106.635 20.8377 61.5636 53.6509 31.1053C54.5312 30.401 55.4115 29.7848 56.3138 29.1466C106.315 -5.99935 143.727 -1.1357 161.818 24.6351C178.807 48.7993 159.793 86.6742 135.695 106.437Z" fill="#9327FF" />
|
||||
<path d="M331.29 161.778C306.531 179.164 267.468 158.873 248.124 134.004C232.036 97.3179 201.402 24.7152 227.04 13.6014C250.148 3.58802 298.543 23.5048 328.935 59.421C361.704 107.992 356.554 144.062 331.29 161.778Z" fill="#00C8FF" />
|
||||
<path d="M298.158 319.642C297.278 320.324 296.42 320.963 295.561 321.557C245.538 356.769 208.126 351.817 190.036 326.09C173.068 301.882 192.038 264.073 216.136 244.289C251.877 228.553 327.758 195.872 339.158 222.061C348.709 244.09 331.015 289.162 298.158 319.642Z" fill="#FFCE00" />
|
||||
<path d="M124.838 337.114C101.818 347.105 53.556 327.276 23.1417 291.514C-9.80345 242.922 -4.60969 206.653 20.6108 188.915C45.3472 171.53 84.4324 191.798 103.755 216.689C119.864 253.397 150.477 325.978 124.838 337.114Z" fill="#FB006D" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -292,3 +292,31 @@ @utility dz-button {
|
|||
@utility xterm {
|
||||
@apply p-2;
|
||||
}
|
||||
|
||||
/* Log line optimization - uses content-visibility for lazy rendering of off-screen log lines */
|
||||
@utility log-line {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: auto 1.5em;
|
||||
}
|
||||
|
||||
/* Search highlight styling for logs */
|
||||
@utility log-highlight {
|
||||
@apply bg-warning/40 dark:bg-warning/30;
|
||||
}
|
||||
|
||||
/* Log level color classes */
|
||||
@utility log-error {
|
||||
@apply bg-red-500/10 dark:bg-red-500/15;
|
||||
}
|
||||
|
||||
@utility log-warning {
|
||||
@apply bg-yellow-500/10 dark:bg-yellow-500/15;
|
||||
}
|
||||
|
||||
@utility log-debug {
|
||||
@apply bg-purple-500/10 dark:bg-purple-500/15;
|
||||
}
|
||||
|
||||
@utility log-info {
|
||||
@apply bg-blue-500/10 dark:bg-blue-500/15;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="flex flex-col items-center justify-center h-32">
|
||||
<span class="text-xl font-bold dark:text-white">You have reached the limit of {{ $name }} you can create.</span>
|
||||
<span>Please <a class="dark:text-white underline "href="{{ route('subscription.show') }}">upgrade your
|
||||
<span>Please <a class="dark:text-white underline" {{ wireNavigate() }} href="{{ route('subscription.show') }}">upgrade your
|
||||
subscription</a> to create more
|
||||
{{ $name }}.</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
}">
|
||||
<div class="flex lg:pt-6 pt-4 pb-4 pl-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<a href="/" class="text-2xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<a href="/" {{ wireNavigate() }} class="text-2xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<x-version />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -105,7 +105,7 @@ class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400
|
|||
<ul role="list" class="flex flex-col h-full space-y-1.5">
|
||||
@if (isSubscribed() || !isCloud())
|
||||
<li>
|
||||
<a title="Dashboard" href="/"
|
||||
<a title="Dashboard" href="/" {{ wireNavigate() }}
|
||||
class="{{ request()->is('/') ? 'menu-item-active menu-item' : 'menu-item' }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
|
|
@ -116,7 +116,7 @@ class="{{ request()->is('/') ? 'menu-item-active menu-item' : 'menu-item' }}">
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Projects"
|
||||
<a title="Projects" {{ wireNavigate() }}
|
||||
class="{{ request()->is('project/*') || request()->is('projects') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="/projects">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"
|
||||
|
|
@ -131,7 +131,7 @@ class="{{ request()->is('project/*') || request()->is('projects') ? 'menu-item m
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Servers"
|
||||
<a title="Servers" {{ wireNavigate() }}
|
||||
class="{{ request()->is('server/*') || request()->is('servers') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
href="/servers">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24"
|
||||
|
|
@ -150,7 +150,7 @@ class="{{ request()->is('server/*') || request()->is('servers') ? 'menu-item men
|
|||
</li>
|
||||
|
||||
<li>
|
||||
<a title="Sources"
|
||||
<a title="Sources" {{ wireNavigate() }}
|
||||
class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('source.all') }}">
|
||||
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
@ -161,7 +161,7 @@ class="{{ request()->is('source*') ? 'menu-item-active menu-item' : 'menu-item'
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Destinations"
|
||||
<a title="Destinations" {{ wireNavigate() }}
|
||||
class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('destination.index') }}">
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ class="{{ request()->is('destination*') ? 'menu-item-active menu-item' : 'menu-i
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="S3 Storages"
|
||||
<a title="S3 Storages" {{ wireNavigate() }}
|
||||
class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('storage.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||
|
|
@ -189,7 +189,7 @@ class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Shared variables"
|
||||
<a title="Shared variables" {{ wireNavigate() }}
|
||||
class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('shared-variables.index') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||
|
|
@ -204,7 +204,7 @@ class="{{ request()->is('shared-variables*') ? 'menu-item-active menu-item' : 'm
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Notifications"
|
||||
<a title="Notifications" {{ wireNavigate() }}
|
||||
class="{{ request()->is('notifications*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('notifications.email') }}">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
|
|
@ -216,7 +216,7 @@ class="{{ request()->is('notifications*') ? 'menu-item-active menu-item' : 'menu
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Keys & Tokens"
|
||||
<a title="Keys & Tokens" {{ wireNavigate() }}
|
||||
class="{{ request()->is('security*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('security.private-key.index') }}">
|
||||
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
@ -228,7 +228,7 @@ class="{{ request()->is('security*') ? 'menu-item-active menu-item' : 'menu-item
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Tags"
|
||||
<a title="Tags" {{ wireNavigate() }}
|
||||
class="{{ request()->is('tags*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('tags.show') }}">
|
||||
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
@ -259,7 +259,7 @@ class="{{ request()->is('terminal*') ? 'menu-item-active menu-item' : 'menu-item
|
|||
</li>
|
||||
@endcan
|
||||
<li>
|
||||
<a title="Profile"
|
||||
<a title="Profile" {{ wireNavigate() }}
|
||||
class="{{ request()->is('profile*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('profile') }}">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
|
|
@ -274,7 +274,7 @@ class="{{ request()->is('profile*') ? 'menu-item-active menu-item' : 'menu-item'
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Teams"
|
||||
<a title="Teams" {{ wireNavigate() }}
|
||||
class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('team.index') }}">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
|
|
@ -293,7 +293,7 @@ class="{{ request()->is('team*') ? 'menu-item-active menu-item' : 'menu-item' }}
|
|||
</li>
|
||||
@if (isCloud() && auth()->user()->isAdmin())
|
||||
<li>
|
||||
<a title="Subscription"
|
||||
<a title="Subscription" {{ wireNavigate() }}
|
||||
class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="{{ route('subscription.show') }}">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
|
|
@ -308,7 +308,7 @@ class="{{ request()->is('subscription*') ? 'menu-item-active menu-item' : 'menu-
|
|||
@if (isInstanceAdmin())
|
||||
<li>
|
||||
|
||||
<a title="Settings"
|
||||
<a title="Settings" {{ wireNavigate() }}
|
||||
class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item' }}"
|
||||
href="/settings">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
|
|
@ -327,7 +327,7 @@ class="{{ request()->is('settings*') ? 'menu-item-active menu-item' : 'menu-item
|
|||
@if (isCloud() || isDev())
|
||||
@if (isInstanceAdmin() || session('impersonating'))
|
||||
<li>
|
||||
<a title="Admin" class="menu-item" href="/admin">
|
||||
<a title="Admin" class="menu-item" href="/admin" {{ wireNavigate() }}>
|
||||
<svg class="text-pink-500 icon" viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
|
|
|
|||
|
|
@ -3,27 +3,27 @@
|
|||
<div class="subtitle">Get notified about your infrastructure.</div>
|
||||
<div class="navbar-main">
|
||||
<nav class="flex items-center gap-6 min-h-10">
|
||||
<a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('notifications.email') }}">
|
||||
<button>Email</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('notifications.discord') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('notifications.discord') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('notifications.discord') }}">
|
||||
<button>Discord</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('notifications.telegram') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('notifications.telegram') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('notifications.telegram') }}">
|
||||
<button>Telegram</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('notifications.slack') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('notifications.slack') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('notifications.slack') }}">
|
||||
<button>Slack</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('notifications.pushover') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('notifications.pushover') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('notifications.pushover') }}">
|
||||
<button>Pushover</button>
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('notifications.webhook') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('notifications.webhook') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('notifications.webhook') }}">
|
||||
<button>Webhook</button>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
<!-- Project Level -->
|
||||
<li class="inline-flex items-center" x-data="{ projectOpen: false, closeTimeout: null, toggle() { this.projectOpen = !this.projectOpen }, open() { clearTimeout(this.closeTimeout); this.projectOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.projectOpen = false }, 100) } }">
|
||||
<div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning"
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
|
||||
href="{{ route('project.show', ['project_uuid' => $currentProjectUuid]) }}">
|
||||
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}
|
||||
</a>
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="absolute z-20 top-full mt-1 w-56 -ml-2 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@foreach ($projects as $project)
|
||||
<a href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}"
|
||||
<a href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $project->uuid === $currentProjectUuid ? 'dark:text-warning font-semibold' : '' }}"
|
||||
title="{{ $project->name }}">
|
||||
{{ $project->name }}
|
||||
|
|
@ -50,7 +50,7 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
|
|||
<li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, envPositions: {}, activeRes: null, resPositions: {}, activeMenuEnv: null, menuPositions: {}, closeTimeout: null, envTimeout: null, resTimeout: null, menuTimeout: null, toggle() { this.envOpen = !this.envOpen; if (!this.envOpen) { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; } }, open() { clearTimeout(this.closeTimeout); this.envOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.envOpen = false; this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openEnv(id) { clearTimeout(this.closeTimeout); clearTimeout(this.envTimeout); this.activeEnv = id }, closeEnv() { this.envTimeout = setTimeout(() => { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openRes(id) { clearTimeout(this.envTimeout); clearTimeout(this.resTimeout); this.activeRes = id }, closeRes() { this.resTimeout = setTimeout(() => { this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openMenu(id) { clearTimeout(this.resTimeout); clearTimeout(this.menuTimeout); this.activeMenuEnv = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenuEnv = null; }, 100) } }">
|
||||
<div class="flex items-center relative" @mouseenter="open()"
|
||||
@mouseleave="close()">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning"
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
|
||||
href="{{ route('project.resource.index', [
|
||||
'environment_uuid' => $currentEnvironmentUuid,
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
|
|
@ -96,7 +96,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
|
|||
<a href="{{ route('project.resource.index', [
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'project_uuid' => $currentProjectUuid,
|
||||
]) }}"
|
||||
]) }}" {{ wireNavigate() }}
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $environment->uuid === $currentEnvironmentUuid ? 'dark:text-warning font-semibold' : '' }}"
|
||||
title="{{ $environment->name }}">
|
||||
<span class="truncate">{{ $environment->name }}</span>
|
||||
|
|
@ -111,7 +111,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
|
|||
</div>
|
||||
@endforeach
|
||||
<div class="border-t border-neutral-200 dark:border-coolgray-200 mt-1 pt-1">
|
||||
<a href="{{ route('project.show', ['project_uuid' => $currentProjectUuid]) }}"
|
||||
<a href="{{ route('project.show', ['project_uuid' => $currentProjectUuid]) }}" {{ wireNavigate() }}
|
||||
class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
|
|
@ -178,7 +178,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
|
|||
@endphp
|
||||
<div @mouseenter="openRes('{{ $environment->uuid }}-{{ $res->uuid }}'); resPositions['{{ $environment->uuid }}-{{ $res->uuid }}'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
|
||||
@mouseleave="closeRes()">
|
||||
<a href="{{ $resRoute }}"
|
||||
<a href="{{ $resRoute }}" {{ wireNavigate() }}
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200 {{ $isCurrentResource ? 'dark:text-warning font-semibold' : '' }}"
|
||||
title="{{ $res->name }}{{ $resServerName ? ' ('.$resServerName.')' : '' }}">
|
||||
<span class="truncate">{{ $res->name }}@if($resServerName) <span class="text-xs text-neutral-400">({{ $resServerName }})</span>@endif</span>
|
||||
|
|
@ -223,7 +223,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
|
|||
@if ($resType === 'application')
|
||||
<div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
|
||||
@mouseleave="closeMenu()">
|
||||
<a href="{{ route('project.application.configuration', $resParams) }}"
|
||||
<a href="{{ route('project.application.configuration', $resParams) }}" {{ wireNavigate() }}
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-3 h-3 shrink-0" fill="none"
|
||||
|
|
@ -233,9 +233,9 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
|
|||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.application.deployment.index', $resParams) }}"
|
||||
<a href="{{ route('project.application.deployment.index', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Deployments</a>
|
||||
<a href="{{ route('project.application.logs', $resParams) }}"
|
||||
<a href="{{ route('project.application.logs', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.application.command', $resParams) }}"
|
||||
|
|
@ -244,7 +244,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
|||
@elseif ($resType === 'service')
|
||||
<div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
|
||||
@mouseleave="closeMenu()">
|
||||
<a href="{{ route('project.service.configuration', $resParams) }}"
|
||||
<a href="{{ route('project.service.configuration', $resParams) }}" {{ wireNavigate() }}
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-3 h-3 shrink-0" fill="none"
|
||||
|
|
@ -254,7 +254,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
|
|||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.service.logs', $resParams) }}"
|
||||
<a href="{{ route('project.service.logs', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.service.command', $resParams) }}"
|
||||
|
|
@ -263,7 +263,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
|||
@else
|
||||
<div @mouseenter="openMenu('{{ $resKey }}-config'); menuPositions['{{ $resKey }}-config'] = $el.offsetTop - ($el.closest('.overflow-y-auto')?.scrollTop || 0)"
|
||||
@mouseleave="closeMenu()">
|
||||
<a href="{{ route('project.database.configuration', $resParams) }}"
|
||||
<a href="{{ route('project.database.configuration', $resParams) }}" {{ wireNavigate() }}
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-3 h-3 shrink-0" fill="none"
|
||||
|
|
@ -273,7 +273,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
|
|||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.database.logs', $resParams) }}"
|
||||
<a href="{{ route('project.database.logs', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Logs</a>
|
||||
@can('canAccessTerminal')
|
||||
<a href="{{ route('project.database.command', $resParams) }}"
|
||||
|
|
@ -284,7 +284,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
|||
$res->getMorphClass() === 'App\Models\StandaloneMongodb' ||
|
||||
$res->getMorphClass() === 'App\Models\StandaloneMysql' ||
|
||||
$res->getMorphClass() === 'App\Models\StandaloneMariadb')
|
||||
<a href="{{ route('project.database.backup.index', $resParams) }}"
|
||||
<a href="{{ route('project.database.backup.index', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">Backups</a>
|
||||
@endif
|
||||
@endif
|
||||
|
|
@ -300,90 +300,90 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
|||
class="pl-1">
|
||||
<div class="w-52 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@if ($resType === 'application')
|
||||
<a href="{{ route('project.application.configuration', $resParams) }}"
|
||||
<a href="{{ route('project.application.configuration', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.application.environment-variables', $resParams) }}"
|
||||
<a href="{{ route('project.application.environment-variables', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
|
||||
Variables</a>
|
||||
<a href="{{ route('project.application.persistent-storage', $resParams) }}"
|
||||
<a href="{{ route('project.application.persistent-storage', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent
|
||||
Storage</a>
|
||||
<a href="{{ route('project.application.source', $resParams) }}"
|
||||
<a href="{{ route('project.application.source', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Source</a>
|
||||
<a href="{{ route('project.application.servers', $resParams) }}"
|
||||
<a href="{{ route('project.application.servers', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a>
|
||||
<a href="{{ route('project.application.scheduled-tasks.show', $resParams) }}"
|
||||
<a href="{{ route('project.application.scheduled-tasks.show', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled
|
||||
Tasks</a>
|
||||
<a href="{{ route('project.application.webhooks', $resParams) }}"
|
||||
<a href="{{ route('project.application.webhooks', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.application.preview-deployments', $resParams) }}"
|
||||
<a href="{{ route('project.application.preview-deployments', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Preview
|
||||
Deployments</a>
|
||||
<a href="{{ route('project.application.healthcheck', $resParams) }}"
|
||||
<a href="{{ route('project.application.healthcheck', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Healthcheck</a>
|
||||
<a href="{{ route('project.application.rollback', $resParams) }}"
|
||||
<a href="{{ route('project.application.rollback', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Rollback</a>
|
||||
<a href="{{ route('project.application.resource-limits', $resParams) }}"
|
||||
<a href="{{ route('project.application.resource-limits', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Limits</a>
|
||||
<a href="{{ route('project.application.resource-operations', $resParams) }}"
|
||||
<a href="{{ route('project.application.resource-operations', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Operations</a>
|
||||
<a href="{{ route('project.application.metrics', $resParams) }}"
|
||||
<a href="{{ route('project.application.metrics', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a>
|
||||
<a href="{{ route('project.application.tags', $resParams) }}"
|
||||
<a href="{{ route('project.application.tags', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.application.advanced', $resParams) }}"
|
||||
<a href="{{ route('project.application.advanced', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Advanced</a>
|
||||
<a href="{{ route('project.application.danger', $resParams) }}"
|
||||
<a href="{{ route('project.application.danger', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
|
||||
Zone</a>
|
||||
@elseif ($resType === 'service')
|
||||
<a href="{{ route('project.service.configuration', $resParams) }}"
|
||||
<a href="{{ route('project.service.configuration', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.service.environment-variables', $resParams) }}"
|
||||
<a href="{{ route('project.service.environment-variables', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
|
||||
Variables</a>
|
||||
<a href="{{ route('project.service.storages', $resParams) }}"
|
||||
<a href="{{ route('project.service.storages', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Storages</a>
|
||||
<a href="{{ route('project.service.scheduled-tasks.show', $resParams) }}"
|
||||
<a href="{{ route('project.service.scheduled-tasks.show', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled
|
||||
Tasks</a>
|
||||
<a href="{{ route('project.service.webhooks', $resParams) }}"
|
||||
<a href="{{ route('project.service.webhooks', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.service.resource-operations', $resParams) }}"
|
||||
<a href="{{ route('project.service.resource-operations', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Operations</a>
|
||||
<a href="{{ route('project.service.tags', $resParams) }}"
|
||||
<a href="{{ route('project.service.tags', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.service.danger', $resParams) }}"
|
||||
<a href="{{ route('project.service.danger', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
|
||||
Zone</a>
|
||||
@else
|
||||
<a href="{{ route('project.database.configuration', $resParams) }}"
|
||||
<a href="{{ route('project.database.configuration', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.database.environment-variables', $resParams) }}"
|
||||
<a href="{{ route('project.database.environment-variables', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
|
||||
Variables</a>
|
||||
<a href="{{ route('project.database.servers', $resParams) }}"
|
||||
<a href="{{ route('project.database.servers', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a>
|
||||
<a href="{{ route('project.database.persistent-storage', $resParams) }}"
|
||||
<a href="{{ route('project.database.persistent-storage', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent
|
||||
Storage</a>
|
||||
<a href="{{ route('project.database.webhooks', $resParams) }}"
|
||||
<a href="{{ route('project.database.webhooks', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.database.resource-limits', $resParams) }}"
|
||||
<a href="{{ route('project.database.resource-limits', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Limits</a>
|
||||
<a href="{{ route('project.database.resource-operations', $resParams) }}"
|
||||
<a href="{{ route('project.database.resource-operations', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Operations</a>
|
||||
<a href="{{ route('project.database.metrics', $resParams) }}"
|
||||
<a href="{{ route('project.database.metrics', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a>
|
||||
<a href="{{ route('project.database.tags', $resParams) }}"
|
||||
<a href="{{ route('project.database.tags', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.database.danger', $resParams) }}"
|
||||
<a href="{{ route('project.database.danger', $resParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
|
||||
Zone</a>
|
||||
@endif
|
||||
|
|
@ -422,7 +422,7 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
|
|||
<li class="inline-flex items-center" x-data="{ resourceOpen: false, activeMenu: null, menuPosition: 0, closeTimeout: null, menuTimeout: null, toggle() { this.resourceOpen = !this.resourceOpen; if (!this.resourceOpen) { this.activeMenu = null; } }, open() { clearTimeout(this.closeTimeout); this.resourceOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.resourceOpen = false; this.activeMenu = null; }, 100) }, openMenu(id) { clearTimeout(this.closeTimeout); clearTimeout(this.menuTimeout); this.activeMenu = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenu = null; }, 100) } }">
|
||||
<div class="flex items-center relative" @mouseenter="open()"
|
||||
@mouseleave="close()">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning"
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
|
||||
href="{{ $isApplication
|
||||
? route('project.application.configuration', $routeParams)
|
||||
: ($isService
|
||||
|
|
@ -451,7 +451,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
|
|||
@if ($isApplication)
|
||||
<!-- Application Main Menus -->
|
||||
<div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()">
|
||||
<a href="{{ route('project.application.configuration', $routeParams) }}"
|
||||
<a href="{{ route('project.application.configuration', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-3 h-3 shrink-0" fill="none" stroke="currentColor"
|
||||
|
|
@ -461,11 +461,11 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
|
|||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.application.deployment.index', $routeParams) }}"
|
||||
<a href="{{ route('project.application.deployment.index', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
Deployments
|
||||
</a>
|
||||
<a href="{{ route('project.application.logs', $routeParams) }}"
|
||||
<a href="{{ route('project.application.logs', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
Logs
|
||||
</a>
|
||||
|
|
@ -478,7 +478,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
|||
@elseif ($isService)
|
||||
<!-- Service Main Menus -->
|
||||
<div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()">
|
||||
<a href="{{ route('project.service.configuration', $routeParams) }}"
|
||||
<a href="{{ route('project.service.configuration', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor"
|
||||
|
|
@ -488,7 +488,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
|
|||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.service.logs', $routeParams) }}"
|
||||
<a href="{{ route('project.service.logs', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
Logs
|
||||
</a>
|
||||
|
|
@ -501,7 +501,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
|||
@else
|
||||
<!-- Database Main Menus -->
|
||||
<div @mouseenter="openMenu('config'); menuPosition = $el.offsetTop" @mouseleave="closeMenu()">
|
||||
<a href="{{ route('project.database.configuration', $routeParams) }}"
|
||||
<a href="{{ route('project.database.configuration', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
<span>Configuration</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor"
|
||||
|
|
@ -511,7 +511,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
|
|||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ route('project.database.logs', $routeParams) }}"
|
||||
<a href="{{ route('project.database.logs', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
Logs
|
||||
</a>
|
||||
|
|
@ -526,7 +526,7 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
|||
$resourceType === 'App\Models\StandaloneMongodb' ||
|
||||
$resourceType === 'App\Models\StandaloneMysql' ||
|
||||
$resourceType === 'App\Models\StandaloneMariadb')
|
||||
<a href="{{ route('project.database.backup.index', $routeParams) }}"
|
||||
<a href="{{ route('project.database.backup.index', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
Backups
|
||||
</a>
|
||||
|
|
@ -543,90 +543,90 @@ class="block px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
|||
class="pl-1">
|
||||
<div class="w-52 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 border border-neutral-200 dark:border-coolgray-200 max-h-96 overflow-y-auto scrollbar">
|
||||
@if ($isApplication)
|
||||
<a href="{{ route('project.application.configuration', $routeParams) }}"
|
||||
<a href="{{ route('project.application.configuration', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.application.environment-variables', $routeParams) }}"
|
||||
<a href="{{ route('project.application.environment-variables', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
|
||||
Variables</a>
|
||||
<a href="{{ route('project.application.persistent-storage', $routeParams) }}"
|
||||
<a href="{{ route('project.application.persistent-storage', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent
|
||||
Storage</a>
|
||||
<a href="{{ route('project.application.source', $routeParams) }}"
|
||||
<a href="{{ route('project.application.source', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Source</a>
|
||||
<a href="{{ route('project.application.servers', $routeParams) }}"
|
||||
<a href="{{ route('project.application.servers', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a>
|
||||
<a href="{{ route('project.application.scheduled-tasks.show', $routeParams) }}"
|
||||
<a href="{{ route('project.application.scheduled-tasks.show', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled
|
||||
Tasks</a>
|
||||
<a href="{{ route('project.application.webhooks', $routeParams) }}"
|
||||
<a href="{{ route('project.application.webhooks', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.application.preview-deployments', $routeParams) }}"
|
||||
<a href="{{ route('project.application.preview-deployments', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Preview
|
||||
Deployments</a>
|
||||
<a href="{{ route('project.application.healthcheck', $routeParams) }}"
|
||||
<a href="{{ route('project.application.healthcheck', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Healthcheck</a>
|
||||
<a href="{{ route('project.application.rollback', $routeParams) }}"
|
||||
<a href="{{ route('project.application.rollback', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Rollback</a>
|
||||
<a href="{{ route('project.application.resource-limits', $routeParams) }}"
|
||||
<a href="{{ route('project.application.resource-limits', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Limits</a>
|
||||
<a href="{{ route('project.application.resource-operations', $routeParams) }}"
|
||||
<a href="{{ route('project.application.resource-operations', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Operations</a>
|
||||
<a href="{{ route('project.application.metrics', $routeParams) }}"
|
||||
<a href="{{ route('project.application.metrics', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a>
|
||||
<a href="{{ route('project.application.tags', $routeParams) }}"
|
||||
<a href="{{ route('project.application.tags', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.application.advanced', $routeParams) }}"
|
||||
<a href="{{ route('project.application.advanced', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Advanced</a>
|
||||
<a href="{{ route('project.application.danger', $routeParams) }}"
|
||||
<a href="{{ route('project.application.danger', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
|
||||
Zone</a>
|
||||
@elseif ($isService)
|
||||
<a href="{{ route('project.service.configuration', $routeParams) }}"
|
||||
<a href="{{ route('project.service.configuration', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.service.environment-variables', $routeParams) }}"
|
||||
<a href="{{ route('project.service.environment-variables', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
|
||||
Variables</a>
|
||||
<a href="{{ route('project.service.storages', $routeParams) }}"
|
||||
<a href="{{ route('project.service.storages', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Storages</a>
|
||||
<a href="{{ route('project.service.scheduled-tasks.show', $routeParams) }}"
|
||||
<a href="{{ route('project.service.scheduled-tasks.show', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Scheduled
|
||||
Tasks</a>
|
||||
<a href="{{ route('project.service.webhooks', $routeParams) }}"
|
||||
<a href="{{ route('project.service.webhooks', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.service.resource-operations', $routeParams) }}"
|
||||
<a href="{{ route('project.service.resource-operations', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Operations</a>
|
||||
<a href="{{ route('project.service.tags', $routeParams) }}"
|
||||
<a href="{{ route('project.service.tags', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.service.danger', $routeParams) }}"
|
||||
<a href="{{ route('project.service.danger', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
|
||||
Zone</a>
|
||||
@else
|
||||
<a href="{{ route('project.database.configuration', $routeParams) }}"
|
||||
<a href="{{ route('project.database.configuration', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">General</a>
|
||||
<a href="{{ route('project.database.environment-variables', $routeParams) }}"
|
||||
<a href="{{ route('project.database.environment-variables', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Environment
|
||||
Variables</a>
|
||||
<a href="{{ route('project.database.servers', $routeParams) }}"
|
||||
<a href="{{ route('project.database.servers', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Servers</a>
|
||||
<a href="{{ route('project.database.persistent-storage', $routeParams) }}"
|
||||
<a href="{{ route('project.database.persistent-storage', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Persistent
|
||||
Storage</a>
|
||||
<a href="{{ route('project.database.webhooks', $routeParams) }}"
|
||||
<a href="{{ route('project.database.webhooks', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Webhooks</a>
|
||||
<a href="{{ route('project.database.resource-limits', $routeParams) }}"
|
||||
<a href="{{ route('project.database.resource-limits', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Limits</a>
|
||||
<a href="{{ route('project.database.resource-operations', $routeParams) }}"
|
||||
<a href="{{ route('project.database.resource-operations', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Resource
|
||||
Operations</a>
|
||||
<a href="{{ route('project.database.metrics', $routeParams) }}"
|
||||
<a href="{{ route('project.database.metrics', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Metrics</a>
|
||||
<a href="{{ route('project.database.tags', $routeParams) }}"
|
||||
<a href="{{ route('project.database.tags', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200">Tags</a>
|
||||
<a href="{{ route('project.database.danger', $routeParams) }}"
|
||||
<a href="{{ route('project.database.danger', $routeParams) }}" {{ wireNavigate() }}
|
||||
class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolgray-200 text-red-500">Danger
|
||||
Zone</a>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@
|
|||
<div class="subtitle">Security related settings.</div>
|
||||
<div class="navbar-main">
|
||||
<nav class="flex items-center gap-6 scrollbar min-h-10">
|
||||
<a href="{{ route('security.private-key.index') }}">
|
||||
<a href="{{ route('security.private-key.index') }}" {{ wireNavigate() }}>
|
||||
<button>Private Keys</button>
|
||||
</a>
|
||||
@can('viewAny', App\Models\CloudProviderToken::class)
|
||||
<a href="{{ route('security.cloud-tokens') }}">
|
||||
<a href="{{ route('security.cloud-tokens') }}" {{ wireNavigate() }}>
|
||||
<button>Cloud Tokens</button>
|
||||
</a>
|
||||
@endcan
|
||||
@can('viewAny', App\Models\CloudInitScript::class)
|
||||
<a href="{{ route('security.cloud-init-scripts') }}">
|
||||
<a href="{{ route('security.cloud-init-scripts') }}" {{ wireNavigate() }}>
|
||||
<button>Cloud-Init Scripts</button>
|
||||
</a>
|
||||
@endcan
|
||||
<a href="{{ route('security.api-tokens') }}">
|
||||
<a href="{{ route('security.api-tokens') }}" {{ wireNavigate() }}>
|
||||
<button>API Tokens</button>
|
||||
</a>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'menu-item menu-item-active' : 'menu-item' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.proxy', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
@if ($server->proxySet())
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
<a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'menu-item menu-item-active' : 'menu-item' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
|
||||
<button>Dynamic Configurations</button>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="{{ request()->routeIs('server.security.patches') ? 'menu-item menu-item-active' : 'menu-item' }}"
|
||||
<a class="{{ request()->routeIs('server.security.patches') ? 'menu-item menu-item-active' : 'menu-item' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.security.patches', $parameters) }}">
|
||||
Server Patching
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}">General</a>
|
||||
@if ($server->isFunctional())
|
||||
<a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.advanced', ['server_uuid' => $server->uuid]) }}">Advanced
|
||||
</a>
|
||||
@endif
|
||||
<a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'private-key' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.private-key', ['server_uuid' => $server->uuid]) }}">Private Key
|
||||
</a>
|
||||
@if ($server->hetzner_server_id)
|
||||
<a class="menu-item {{ $activeMenu === 'cloud-provider-token' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'cloud-provider-token' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.cloud-provider-token', ['server_uuid' => $server->uuid]) }}">Hetzner Token
|
||||
</a>
|
||||
@endif
|
||||
<a class="menu-item {{ $activeMenu === 'ca-certificate' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'ca-certificate' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.ca-certificate', ['server_uuid' => $server->uuid]) }}">CA Certificate
|
||||
</a>
|
||||
@if (!$server->isLocalhost())
|
||||
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnel' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'cloudflare-tunnel' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.cloudflare-tunnel', ['server_uuid' => $server->uuid]) }}">Cloudflare
|
||||
Tunnel</a>
|
||||
@endif
|
||||
@if ($server->isFunctional())
|
||||
<a class="menu-item {{ $activeMenu === 'docker-cleanup' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'docker-cleanup' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.docker-cleanup', ['server_uuid' => $server->uuid]) }}">Docker Cleanup
|
||||
</a>
|
||||
<a class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'destinations' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.destinations', ['server_uuid' => $server->uuid]) }}">Destinations
|
||||
</a>
|
||||
<a class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'log-drains' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.log-drains', ['server_uuid' => $server->uuid]) }}">Log
|
||||
Drains</a>
|
||||
<a class="menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'metrics' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.charts', ['server_uuid' => $server->uuid]) }}">Metrics</a>
|
||||
@endif
|
||||
@if (!$server->isLocalhost())
|
||||
<a class="menu-item {{ $activeMenu === 'danger' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'danger' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('server.delete', ['server_uuid' => $server->uuid]) }}">Danger</a>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@
|
|||
<div class="subtitle">Instance wide settings for Coolify.</div>
|
||||
<div class="navbar-main">
|
||||
<nav class="flex items-center gap-6 min-h-10 whitespace-nowrap">
|
||||
<a class="{{ request()->routeIs('settings.index') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('settings.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.index') }}">
|
||||
Configuration
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('settings.backup') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('settings.backup') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.backup') }}">
|
||||
Backup
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('settings.email') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('settings.email') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.email') }}">
|
||||
Transactional Email
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('settings.oauth') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('settings.oauth') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.oauth') }}">
|
||||
OAuth
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'general' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.index') }}">General</a>
|
||||
<a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'advanced' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.advanced') }}">Advanced</a>
|
||||
<a class="menu-item {{ $activeMenu === 'updates' ? 'menu-item-active' : '' }}"
|
||||
<a class="menu-item {{ $activeMenu === 'updates' ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.updates') }}">Updates</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@
|
|||
<div class="subtitle">Team wide configurations.</div>
|
||||
<div class="navbar-main">
|
||||
<nav class="flex items-center gap-6 min-h-10">
|
||||
<a class="{{ request()->routeIs('team.index') ? 'dark:text-white' : '' }}" href="{{ route('team.index') }}">
|
||||
<a class="{{ request()->routeIs('team.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('team.index') }}">
|
||||
General
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('team.member.index') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('team.member.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('team.member.index') }}">
|
||||
Members
|
||||
</a>
|
||||
@if (isInstanceAdmin())
|
||||
<a class="{{ request()->routeIs('team.admin-view') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('team.admin-view') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('team.admin-view') }}">
|
||||
Admin View
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
The currentStep variable is inherited from parent Alpine component (upgradeModal).
|
||||
--}}
|
||||
<div class="w-full max-w-md mx-auto" x-data="{ activeStep: {{ $step }} }" x-effect="activeStep = $el.closest('[x-data]')?.__x?.$data?.currentStep ?? {{ $step }}">
|
||||
<div class="w-full" x-data="{ activeStep: {{ $step }} }" x-effect="activeStep = $el.closest('[x-data]')?.__x?.$data?.currentStep ?? {{ $step }}">
|
||||
<div class="flex items-center justify-between">
|
||||
{{-- Step 1: Preparing --}}
|
||||
<div class="flex items-center flex-1">
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">404</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">How did you get here?</h1>
|
||||
<p class="text-base leading-7 dark:text-neutral-400 text-black">Sorry, we couldn’t find the page you’re looking
|
||||
<p class="text-base leading-7 dark:text-neutral-400 text-black">Sorry, we couldn't find the page you're looking
|
||||
for.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-2">
|
||||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">419</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">This page is definitely old, not like you!</h1>
|
||||
<p class="text-base leading-7 dark:text-neutral-300 text-black">Sorry, we couldn’t find the page you’re looking
|
||||
<p class="text-base leading-7 dark:text-neutral-300 text-black">Sorry, we couldn't find the page you're looking
|
||||
for.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-2">
|
||||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
|
|||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||
@foreach ($projects as $project)
|
||||
<div class="relative gap-2 cursor-pointer coolbox group">
|
||||
<a href="{{ $project->navigateTo() }}" class="absolute inset-0"></a>
|
||||
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }} class="absolute inset-0"></a>
|
||||
<div class="flex flex-1 mx-6">
|
||||
<div class="flex flex-col justify-center flex-1">
|
||||
<div class="box-title">{{ $project->name }}</div>
|
||||
|
|
@ -47,7 +47,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
|
|||
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold">
|
||||
@if ($project->environments->first())
|
||||
@can('createAnyResource')
|
||||
<a class="hover:underline"
|
||||
<a class="hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.resource.create', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $project->environments->first()->uuid,
|
||||
|
|
@ -57,7 +57,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
|
|||
@endcan
|
||||
@endif
|
||||
@can('update', $project)
|
||||
<a class="hover:underline"
|
||||
<a class="hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
|
|
@ -74,7 +74,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
|
|||
<x-modal-input buttonTitle="Add" title="New Project">
|
||||
<livewire:project.add-empty />
|
||||
</x-modal-input> your first project or
|
||||
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}">onboarding</a> page.
|
||||
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a> page.
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -101,7 +101,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
|
|||
@if ($servers->count() > 0)
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||
@foreach ($servers as $server)
|
||||
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
|
||||
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" {{ wireNavigate() }}
|
||||
@class([
|
||||
'gap-2 border cursor-pointer coolbox group',
|
||||
'border-red-500' =>
|
||||
|
|
@ -138,7 +138,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
|
|||
<livewire:security.private-key.create from="server" />
|
||||
</x-modal-input> a private key
|
||||
or
|
||||
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}">onboarding</a>
|
||||
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a>
|
||||
page.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -150,7 +150,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
|
|||
<livewire:server.create />
|
||||
</x-modal-input> your first server
|
||||
or
|
||||
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}">onboarding</a>
|
||||
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}" {{ wireNavigate() }}>onboarding</a>
|
||||
page.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class="absolute bottom-full mb-2 w-80 max-h-96 overflow-y-auto rounded-lg shadow
|
|||
|
||||
<div class="p-4 space-y-3">
|
||||
@foreach ($this->deployments as $deployment)
|
||||
<a href="{{ $deployment->deployment_url }}"
|
||||
<a href="{{ $deployment->deployment_url }}" {{ wireNavigate() }}
|
||||
class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 transition-all duration-200 hover:ring-2 hover:ring-coollabs dark:hover:ring-warning cursor-pointer">
|
||||
<!-- Status indicator -->
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
@forelse ($servers as $server)
|
||||
@forelse ($server->destinations() as $destination)
|
||||
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
|
||||
<a class="coolbox group"
|
||||
<a class="coolbox group" {{ wireNavigate() }}
|
||||
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
|
||||
<div class="flex flex-col justify-center mx-6">
|
||||
<div class="box-title">{{ $destination->name }}</div>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
</a>
|
||||
@endif
|
||||
@if ($destination->getMorphClass() === 'App\Models\SwarmDocker')
|
||||
<a class="coolbox group"
|
||||
<a class="coolbox group" {{ wireNavigate() }}
|
||||
href="{{ route('destination.show', ['destination_uuid' => data_get($destination, 'uuid')]) }}">
|
||||
<div class="flex flex-col mx-6">
|
||||
<div class="box-title">{{ $destination->name }}</div>
|
||||
|
|
|
|||
|
|
@ -70,6 +70,11 @@
|
|||
},
|
||||
|
||||
openModal() {
|
||||
// Check if $wire is available (may not be after SPA navigation destroys/recreates component)
|
||||
if (typeof $wire === 'undefined' || !$wire) {
|
||||
console.warn('Global search: $wire not available, skipping open');
|
||||
return;
|
||||
}
|
||||
this.modalOpen = true;
|
||||
this.selectedIndex = -1;
|
||||
this.isLoadingInitialData = true;
|
||||
|
|
@ -79,6 +84,10 @@
|
|||
this.creatableItems = $wire.creatableItems || [];
|
||||
this.isLoadingInitialData = false;
|
||||
setTimeout(() => this.$refs.searchInput?.focus(), 50);
|
||||
}).catch(() => {
|
||||
// Handle case where component was destroyed during navigation
|
||||
this.modalOpen = false;
|
||||
this.isLoadingInitialData = false;
|
||||
});
|
||||
},
|
||||
closeModal() {
|
||||
|
|
@ -90,7 +99,10 @@
|
|||
this.allSearchableItems = [];
|
||||
// Ensure scroll is restored
|
||||
document.body.style.overflow = '';
|
||||
@this.closeSearchModal();
|
||||
// Use $wire instead of @this for SPA navigation compatibility
|
||||
if ($wire) {
|
||||
$wire.closeSearchModal();
|
||||
}
|
||||
},
|
||||
navigateResults(direction) {
|
||||
const results = document.querySelectorAll('.search-result-item');
|
||||
|
|
@ -120,7 +132,7 @@
|
|||
const trimmed = value.trim().toLowerCase();
|
||||
|
||||
if (trimmed === '') {
|
||||
if ($wire.isSelectingResource) {
|
||||
if (typeof $wire !== 'undefined' && $wire && $wire.isSelectingResource) {
|
||||
$wire.cancelResourceSelection();
|
||||
}
|
||||
return;
|
||||
|
|
@ -149,7 +161,7 @@
|
|||
(item.quickcommand && item.quickcommand.toLowerCase().includes(trimmed));
|
||||
});
|
||||
|
||||
if (matchingItem) {
|
||||
if (matchingItem && typeof $wire !== 'undefined' && $wire) {
|
||||
$wire.navigateToResource(matchingItem.type);
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +198,7 @@
|
|||
// If search query is empty, close the modal
|
||||
if (!this.searchQuery || this.searchQuery === '') {
|
||||
// Check if we're in a selection state using Alpine-accessible Livewire state
|
||||
if ($wire.isSelectingResource) {
|
||||
if (typeof $wire !== 'undefined' && $wire && $wire.isSelectingResource) {
|
||||
$wire.cancelResourceSelection();
|
||||
setTimeout(() => this.$refs.searchInput?.focus(), 100);
|
||||
} else {
|
||||
|
|
@ -227,19 +239,23 @@
|
|||
document.removeEventListener('keydown', arrowKeyHandler);
|
||||
});
|
||||
|
||||
// Watch for auto-open resource
|
||||
this.$watch('$wire.autoOpenResource', value => {
|
||||
if (value) {
|
||||
// Close search modal first
|
||||
this.closeModal();
|
||||
// Open the specific resource modal after a short delay
|
||||
setTimeout(() => {
|
||||
this.$dispatch('open-create-modal-' + value);
|
||||
// Reset the value so it can trigger again
|
||||
@this.set('autoOpenResource', null);
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
// Watch for auto-open resource (only if $wire is available)
|
||||
if (typeof $wire !== 'undefined' && $wire) {
|
||||
this.$watch('$wire.autoOpenResource', value => {
|
||||
if (value) {
|
||||
// Close search modal first
|
||||
this.closeModal();
|
||||
// Open the specific resource modal after a short delay
|
||||
setTimeout(() => {
|
||||
this.$dispatch('open-create-modal-' + value);
|
||||
// Reset the value so it can trigger again
|
||||
if (typeof $wire !== 'undefined' && $wire) {
|
||||
$wire.set('autoOpenResource', null);
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for closeSearchModal event from backend
|
||||
window.addEventListener('closeSearchModal', () => {
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class="font-bold dark:text-white">Stripe</a></x-forms.button>
|
|||
<div><span class="font-bold text-red-500">WARNING:</span> Your subscription is in over-due. If your
|
||||
latest
|
||||
payment is not paid within a week, all automations <span class="font-bold text-red-500">will
|
||||
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}"
|
||||
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}" {{ wireNavigate() }}
|
||||
class="underline dark:text-white">/subscription</a> to check your subscription status or pay
|
||||
your
|
||||
invoice (or check your email for the invoice).
|
||||
|
|
@ -148,7 +148,7 @@ class="underline dark:text-white">/subscription</a> to check your subscription s
|
|||
<x-banner :closable=false>
|
||||
<div><span class="font-bold text-red-500">WARNING:</span> The number of active servers exceeds the limit
|
||||
covered by your payment. If not resolved, some of your servers <span class="font-bold text-red-500">will
|
||||
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}"
|
||||
be deactivated</span>. Visit <a href="{{ route('subscription.show') }}" {{ wireNavigate() }}
|
||||
class="underline dark:text-white">/subscription</a> to update your subscription or remove some
|
||||
servers.
|
||||
</div>
|
||||
|
|
@ -172,7 +172,7 @@ class="underline dark:text-white">/subscription</a> to update your subscription
|
|||
highly recommended to enable at least
|
||||
one
|
||||
notification channel to receive important alerts.<br>Visit <a
|
||||
href="{{ route('notifications.email') }}" class="underline dark:text-white">/notification</a> to
|
||||
href="{{ route('notifications.email') }}" {{ wireNavigate() }} class="underline dark:text-white">/notification</a> to
|
||||
enable notifications.</span>
|
||||
</x-slot:description>
|
||||
<x-slot:button-text @click="disableNotification()">
|
||||
|
|
|
|||
|
|
@ -8,27 +8,27 @@
|
|||
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">General</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.advanced', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Advanced</a>
|
||||
@if ($application->destination->server->isSwarm())
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.swarm', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Swarm
|
||||
Configuration</a>
|
||||
@endif
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Environment
|
||||
Variables</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Persistent
|
||||
Storage</a>
|
||||
@if ($application->git_based())
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.source', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Git
|
||||
Source</a>
|
||||
@endif
|
||||
<a class="menu-item flex items-center gap-2" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item flex items-center gap-2" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Servers
|
||||
@if ($application->server_status == false)
|
||||
<span title="One or more servers are unreachable or misconfigured.">
|
||||
|
|
@ -46,33 +46,33 @@
|
|||
</span>
|
||||
@endif
|
||||
</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Scheduled
|
||||
Tasks</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Webhooks</a>
|
||||
@if ($application->deploymentType() !== 'deploy_key')
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.preview-deployments', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Preview
|
||||
Deployments</a>
|
||||
@endif
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Healthcheck</a>
|
||||
@endif
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.rollback', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Rollback</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Resource
|
||||
Limits</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Resource
|
||||
Operations</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Metrics</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Tags</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.application.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Danger
|
||||
Zone</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
'border-error' => data_get($deployment, 'status') === 'failed',
|
||||
'border-success' => data_get($deployment, 'status') === 'finished',
|
||||
])>
|
||||
<a href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}" class="block">
|
||||
<a href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}" {{ wireNavigate() }} class="block">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span @class([
|
||||
|
|
|
|||
|
|
@ -8,26 +8,44 @@
|
|||
<div x-data="{
|
||||
fullscreen: @entangle('fullscreen'),
|
||||
alwaysScroll: {{ $isKeepAliveOn ? 'true' : 'false' }},
|
||||
intervalId: null,
|
||||
rafId: null,
|
||||
showTimestamps: true,
|
||||
searchQuery: '',
|
||||
renderTrigger: 0,
|
||||
deploymentId: '{{ $application_deployment_queue->deployment_uuid ?? 'deployment' }}',
|
||||
// Cache for decoded HTML to avoid repeated DOMParser calls
|
||||
decodeCache: new Map(),
|
||||
// Cache for match count to avoid repeated DOM queries
|
||||
matchCountCache: null,
|
||||
lastSearchQuery: '',
|
||||
makeFullscreen() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
},
|
||||
scrollToBottom() {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
},
|
||||
scheduleScroll() {
|
||||
if (!this.alwaysScroll) return;
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
this.scrollToBottom();
|
||||
// Schedule next scroll after a reasonable delay (250ms instead of 100ms)
|
||||
if (this.alwaysScroll) {
|
||||
setTimeout(() => this.scheduleScroll(), 250);
|
||||
}
|
||||
});
|
||||
},
|
||||
toggleScroll() {
|
||||
this.alwaysScroll = !this.alwaysScroll;
|
||||
if (this.alwaysScroll) {
|
||||
this.intervalId = setInterval(() => {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
}, 100);
|
||||
this.scheduleScroll();
|
||||
} else {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
matchesSearch(text) {
|
||||
|
|
@ -41,17 +59,19 @@
|
|||
}
|
||||
const logsContainer = document.getElementById('logs');
|
||||
if (!logsContainer) return false;
|
||||
|
||||
// Check if selection is within the logs container
|
||||
const range = selection.getRangeAt(0);
|
||||
return logsContainer.contains(range.commonAncestorContainer);
|
||||
},
|
||||
decodeHtml(text) {
|
||||
// Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS
|
||||
// Return cached result if available
|
||||
if (this.decodeCache.has(text)) {
|
||||
return this.decodeCache.get(text);
|
||||
}
|
||||
// Decode HTML entities with max iteration limit
|
||||
let decoded = text;
|
||||
let prev = '';
|
||||
let iterations = 0;
|
||||
const maxIterations = 3; // Prevent DoS from deeply nested HTML entities
|
||||
const maxIterations = 3;
|
||||
|
||||
while (decoded !== prev && iterations < maxIterations) {
|
||||
prev = decoded;
|
||||
|
|
@ -59,11 +79,17 @@
|
|||
decoded = doc.documentElement.textContent;
|
||||
iterations++;
|
||||
}
|
||||
// Cache the result (limit cache size to prevent memory bloat)
|
||||
if (this.decodeCache.size > 5000) {
|
||||
// Clear oldest entries when cache gets too large
|
||||
const firstKey = this.decodeCache.keys().next().value;
|
||||
this.decodeCache.delete(firstKey);
|
||||
}
|
||||
this.decodeCache.set(text, decoded);
|
||||
return decoded;
|
||||
},
|
||||
renderHighlightedLog(el, text) {
|
||||
// Skip re-render if user has text selected in logs (preserves copy ability)
|
||||
// But always render if the element is empty (initial render)
|
||||
// Skip re-render if user has text selected in logs
|
||||
if (el.textContent && this.hasActiveLogSelection()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -82,11 +108,9 @@
|
|||
|
||||
let index = lowerText.indexOf(query, lastIndex);
|
||||
while (index !== -1) {
|
||||
// Add text before match
|
||||
if (index > lastIndex) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex, index)));
|
||||
}
|
||||
// Add highlighted match
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'log-highlight';
|
||||
mark.textContent = decoded.substring(index, index + this.searchQuery.length);
|
||||
|
|
@ -96,22 +120,28 @@
|
|||
index = lowerText.indexOf(query, lastIndex);
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < decoded.length) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex)));
|
||||
}
|
||||
},
|
||||
getMatchCount() {
|
||||
if (!this.searchQuery.trim()) return 0;
|
||||
// Return cached count if search query hasn't changed
|
||||
if (this.lastSearchQuery === this.searchQuery && this.matchCountCache !== null) {
|
||||
return this.matchCountCache;
|
||||
}
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return 0;
|
||||
const lines = logs.querySelectorAll('[data-log-line]');
|
||||
let count = 0;
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
lines.forEach(line => {
|
||||
if (line.dataset.logContent && line.dataset.logContent.toLowerCase().includes(this.searchQuery.toLowerCase())) {
|
||||
if (line.dataset.logContent && line.dataset.logContent.toLowerCase().includes(query)) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
this.matchCountCache = count;
|
||||
this.lastSearchQuery = this.searchQuery;
|
||||
return count;
|
||||
},
|
||||
downloadLogs() {
|
||||
|
|
@ -135,15 +165,11 @@
|
|||
URL.revokeObjectURL(url);
|
||||
},
|
||||
stopScroll() {
|
||||
// Scroll to the end one final time before disabling
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
this.scrollToBottom();
|
||||
this.alwaysScroll = false;
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
},
|
||||
init() {
|
||||
|
|
@ -153,30 +179,32 @@
|
|||
skip();
|
||||
}
|
||||
});
|
||||
// Re-render logs after Livewire updates
|
||||
// Re-render logs after Livewire updates (debounced)
|
||||
let renderTimeout = null;
|
||||
const debouncedRender = () => {
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(() => {
|
||||
this.matchCountCache = null; // Invalidate match cache on new content
|
||||
this.renderTrigger++;
|
||||
}, 100);
|
||||
};
|
||||
document.addEventListener('livewire:navigated', () => {
|
||||
this.$nextTick(() => { this.renderTrigger++; });
|
||||
this.$nextTick(debouncedRender);
|
||||
});
|
||||
Livewire.hook('commit', ({ succeed }) => {
|
||||
succeed(() => {
|
||||
this.$nextTick(() => { this.renderTrigger++; });
|
||||
this.$nextTick(debouncedRender);
|
||||
});
|
||||
});
|
||||
// Stop auto-scroll when deployment finishes
|
||||
Livewire.on('deploymentFinished', () => {
|
||||
// Wait for DOM to update with final logs before scrolling to end
|
||||
setTimeout(() => {
|
||||
this.stopScroll();
|
||||
}, 500);
|
||||
});
|
||||
// Start auto-scroll if deployment is in progress
|
||||
if (this.alwaysScroll) {
|
||||
this.intervalId = setInterval(() => {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
}
|
||||
}, 100);
|
||||
this.scheduleScroll();
|
||||
}
|
||||
}
|
||||
}">
|
||||
|
|
@ -212,7 +240,7 @@ class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
|||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<input type="text" x-model="searchQuery" placeholder="Find in logs"
|
||||
<input type="text" x-model.debounce.300ms="searchQuery" placeholder="Find in logs"
|
||||
class="input input-sm w-48 pl-8 pr-8 dark:bg-coolgray-200" />
|
||||
<button x-show="searchQuery" x-on:click="searchQuery = ''" type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||
|
|
@ -222,6 +250,21 @@ class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-6
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
x-on:click="
|
||||
$wire.copyLogs().then(logs => {
|
||||
navigator.clipboard.writeText(logs);
|
||||
Livewire.dispatch('success', ['Logs copied to clipboard.']);
|
||||
});
|
||||
"
|
||||
title="Copy Logs"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
</button>
|
||||
<button x-on:click="downloadLogs()" title="Download Logs"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
|
|
@ -293,7 +336,7 @@ class="text-gray-500 dark:text-gray-400 py-2">
|
|||
<div data-log-line data-log-content="{{ htmlspecialchars($searchableContent) }}"
|
||||
x-effect="renderTrigger; searchQuery; $el.classList.toggle('hidden', !matchesSearch($el.dataset.logContent))" @class([
|
||||
'mt-2' => isset($line['command']) && $line['command'],
|
||||
'flex gap-2',
|
||||
'flex gap-2 log-line',
|
||||
])>
|
||||
<span x-show="showTimestamps"
|
||||
class="shrink-0 text-gray-500">{{ $line['timestamp'] }}</span>
|
||||
|
|
|
|||
|
|
@ -236,11 +236,7 @@
|
|||
@endif
|
||||
<div class="flex flex-col gap-2 pt-6 pb-10">
|
||||
@if ($application->build_pack === 'dockercompose')
|
||||
@can('update', $application)
|
||||
<div class="flex flex-col gap-2" x-init="$wire.dispatch('loadCompose', true)">
|
||||
@else
|
||||
<div class="flex flex-col gap-2">
|
||||
@endcan
|
||||
<div class="flex flex-col gap-2" @can('update', $application) x-init="$wire.dispatch('loadCompose', true)" @endcan>
|
||||
<div x-data="{
|
||||
baseDir: '{{ $application->base_directory }}',
|
||||
composeLocation: '{{ $application->docker_compose_location }}',
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
<div class="navbar-main">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.configuration', $parameters) }}">
|
||||
Configuration
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('project.application.deployment.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.deployment.index', $parameters) }}">
|
||||
Deployments
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -94,12 +94,12 @@ class="dark:text-warning">{{ $application->destination->server->name }}</span>.<
|
|||
</a>
|
||||
@if (count($parameters) > 0)
|
||||
|
|
||||
<a
|
||||
<a {{ wireNavigate() }}
|
||||
href="{{ route('project.application.deployment.index', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
Deployment Logs
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
<a {{ wireNavigate() }}
|
||||
href="{{ route('project.application.logs', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
Application Logs
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -7,34 +7,34 @@
|
|||
<livewire:project.database.heading :database="$database" />
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">General</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Environment
|
||||
Variables</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Servers</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Persistent
|
||||
Storage</a>
|
||||
@can('update', $database)
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.import-backups', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Import
|
||||
Backups</a>
|
||||
@endcan
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Webhooks</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Resource
|
||||
Limits</a>
|
||||
<a class="menu-item" wire:current.exact="menu-item-active"
|
||||
<a class="menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Resource
|
||||
Operations</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.metrics', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Metrics</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Tags</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Danger
|
||||
Zone</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="navbar-main">
|
||||
<nav
|
||||
class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:overflow-x-hidden scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.database.configuration', $parameters) }}">
|
||||
Configuration
|
||||
</a>
|
||||
|
|
@ -31,7 +31,7 @@ class="flex overflow-x-scroll shrink-0 gap-6 items-center whitespace-nowrap sm:o
|
|||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
|
||||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
|
||||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
|
||||
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.database.backup.index', $parameters) }}">
|
||||
Backups
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
@endif
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
|
||||
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
@endif
|
||||
<div class="pt-2">
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' {{ wireNavigate() }} href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
|
||||
id="customDockerRunOptions" label="Custom Docker Options" canGate="update"
|
||||
:canResource="$database" />
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
</div>
|
||||
@endif
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' {{ wireNavigate() }} href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
|
||||
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
@endif
|
||||
<div class="pt-2">
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
|
||||
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
placeholder="If empty, use default. See in docker docs." />
|
||||
</div>
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' {{ wireNavigate() }} href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
|
||||
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
@endif
|
||||
</div>
|
||||
<x-forms.input
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
helper="You can add custom docker run options that will be used when your container is started.<br>Note: Not all options are supported, as they could mess up Coolify's automation and could cause bad experience for users.<br><br>Check the <a class='underline dark:text-white' {{ wireNavigate() }} href='https://coolify.io/docs/knowledge-base/docker/custom-commands'>docs.</a>"
|
||||
placeholder="--cap-add SYS_ADMIN --device=/dev/fuse --security-opt apparmor:unconfined --ulimit nofile=1024:1024 --tmpfs /run:rw,noexec,nosuid,size=65536k"
|
||||
id="customDockerRunOptions" label="Custom Docker Options" canGate="update" :canResource="$database" />
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
$backup->latest_log &&
|
||||
data_get($backup->latest_log, 'status') === 'success',
|
||||
'border-gray-200 dark:border-coolgray-300' => !$backup->latest_log,
|
||||
])
|
||||
]) {{ wireNavigate() }}
|
||||
href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
|
||||
@if ($backup->latest_log && data_get($backup->latest_log, 'status') === 'running')
|
||||
<div class="absolute top-2 right-2">
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@
|
|||
<ol class="flex flex-wrap items-center gap-y-1">
|
||||
<li class="inline-flex items-center">
|
||||
<div class="flex items-center">
|
||||
<a class="text-xs truncate lg:text-sm"
|
||||
<a class="text-xs truncate lg:text-sm" {{ wireNavigate() }}
|
||||
href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}">
|
||||
{{ $project->name }}</a>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<a class="text-xs truncate lg:text-sm"
|
||||
<a class="text-xs truncate lg:text-sm" {{ wireNavigate() }}
|
||||
href="{{ route('project.resource.index', ['environment_uuid' => $environment->uuid, 'project_uuid' => $project->uuid]) }}">
|
||||
{{ $environment->name }}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold">
|
||||
@if ($project->environments->first())
|
||||
@can('createAnyResource')
|
||||
<a class="hover:underline"
|
||||
<a class="hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.resource.create', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $project->environments->first()->uuid,
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
@endcan
|
||||
@endif
|
||||
@can('update', $project)
|
||||
<a class="hover:underline"
|
||||
<a class="hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class="loading loading-xs dark:text-warning loading-spinner"></span>
|
|||
<div>
|
||||
No private keys found.
|
||||
</div>
|
||||
<a href="{{ route('security.private-key.index') }}">
|
||||
<a href="{{ route('security.private-key.index') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Create a new private key</x-forms.button>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ function searchResources() {
|
|||
<div class="flex flex-col justify-center gap-4 text-left xl:flex-row xl:flex-wrap">
|
||||
@if ($onlyBuildServerAvailable)
|
||||
<div> Only build servers are available, you need at least one server that is not set as build
|
||||
server. <a class="underline dark:text-white" href="/servers">
|
||||
server. <a class="underline dark:text-white" href="/servers" {{ wireNavigate() }}>
|
||||
Go to servers page
|
||||
</a> </div>
|
||||
@else
|
||||
|
|
@ -404,7 +404,7 @@ function searchResources() {
|
|||
<div>
|
||||
|
||||
<div>No validated & reachable servers found. <a class="underline dark:text-white"
|
||||
href="/servers">
|
||||
href="/servers" {{ wireNavigate() }}>
|
||||
Go to servers page
|
||||
</a></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@
|
|||
<h1>Resources</h1>
|
||||
@if ($environment->isEmpty())
|
||||
@can('createAnyResource')
|
||||
<a class="button"
|
||||
<a class="button" {{ wireNavigate() }}
|
||||
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}">
|
||||
Clone
|
||||
</a>
|
||||
@endcan
|
||||
@else
|
||||
@can('createAnyResource')
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" {{ wireNavigate() }}
|
||||
class="button">+
|
||||
New</a>
|
||||
@endcan
|
||||
@can('createAnyResource')
|
||||
<a class="button"
|
||||
<a class="button" {{ wireNavigate() }}
|
||||
href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}">
|
||||
Clone
|
||||
</a>
|
||||
|
|
@ -36,7 +36,7 @@ class="button">+
|
|||
<ol class="flex items-center">
|
||||
<li class="inline-flex items-center" x-data="{ projectOpen: false, toggle() { this.projectOpen = !this.projectOpen }, open() { this.projectOpen = true }, close() { this.projectOpen = false } }">
|
||||
<div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning"
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
|
||||
href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}">
|
||||
{{ $project->name }}</a>
|
||||
<button type="button" @click.stop="toggle()" class="px-1 text-warning">
|
||||
|
|
@ -66,7 +66,7 @@ class="block px-4 py-2 text-sm truncate hover:bg-neutral-100 dark:hover:bg-coolg
|
|||
@endphp
|
||||
<li class="inline-flex items-center" x-data="{ envOpen: false, activeEnv: null, envPositions: {}, activeRes: null, resPositions: {}, activeMenuEnv: null, menuPositions: {}, closeTimeout: null, envTimeout: null, resTimeout: null, menuTimeout: null, toggle() { this.envOpen = !this.envOpen; if (!this.envOpen) { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; } }, open() { clearTimeout(this.closeTimeout); this.envOpen = true }, close() { this.closeTimeout = setTimeout(() => { this.envOpen = false; this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openEnv(id) { clearTimeout(this.closeTimeout); clearTimeout(this.envTimeout); this.activeEnv = id }, closeEnv() { this.envTimeout = setTimeout(() => { this.activeEnv = null; this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openRes(id) { clearTimeout(this.envTimeout); clearTimeout(this.resTimeout); this.activeRes = id }, closeRes() { this.resTimeout = setTimeout(() => { this.activeRes = null; this.activeMenuEnv = null; }, 100) }, openMenu(id) { clearTimeout(this.resTimeout); clearTimeout(this.menuTimeout); this.activeMenuEnv = id }, closeMenu() { this.menuTimeout = setTimeout(() => { this.activeMenuEnv = null; }, 100) } }">
|
||||
<div class="flex items-center relative" @mouseenter="open()" @mouseleave="close()">
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning"
|
||||
<a class="text-xs truncate lg:text-sm hover:text-warning" {{ wireNavigate() }}
|
||||
href="{{ route('project.resource.index', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => $environment->uuid]) }}">
|
||||
{{ $environment->name }}
|
||||
</a>
|
||||
|
|
@ -106,7 +106,7 @@ class="flex items-center justify-between gap-2 px-4 py-2 text-sm hover:bg-neutra
|
|||
</div>
|
||||
@endforeach
|
||||
<div class="border-t border-neutral-200 dark:border-coolgray-200 mt-1 pt-1">
|
||||
<a href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}"
|
||||
<a href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}" {{ wireNavigate() }}
|
||||
class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-coolgray-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
|
|
@ -311,7 +311,7 @@ class="pl-1">
|
|||
</div>
|
||||
@if ($environment->isEmpty())
|
||||
@can('createAnyResource')
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}"
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => data_get($parameters, 'project_uuid'), 'environment_uuid' => data_get($environment, 'uuid')]) }}" {{ wireNavigate() }}
|
||||
class="items-center justify-center coolbox">+ Add Resource</a>
|
||||
@else
|
||||
<div
|
||||
|
|
@ -352,7 +352,7 @@ class="font-semibold" x-text="search"></span>".</p>
|
|||
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.uuid">
|
||||
<span>
|
||||
<a class="h-24 coolbox group" :href="item.hrefLink">
|
||||
<a class="h-24 coolbox group" :href="item.hrefLink" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex gap-2 px-4">
|
||||
<div class="pb-2 truncate box-title" x-text="item.name"></div>
|
||||
|
|
@ -402,7 +402,7 @@ class="flex flex-wrap gap-1 pt-1 dark:group-hover:text-white group-hover:text-bl
|
|||
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<template x-for="item in filteredDatabases" :key="item.uuid">
|
||||
<span>
|
||||
<a class="h-24 coolbox group" :href="item.hrefLink">
|
||||
<a class="h-24 coolbox group" :href="item.hrefLink" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex gap-2 px-4">
|
||||
<div class="pb-2 truncate box-title" x-text="item.name"></div>
|
||||
|
|
@ -452,7 +452,7 @@ class="flex flex-wrap gap-1 pt-1 dark:group-hover:text-white group-hover:text-bl
|
|||
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<template x-for="item in filteredServices" :key="item.uuid">
|
||||
<span>
|
||||
<a class="h-24 coolbox group" :href="item.hrefLink">
|
||||
<a class="h-24 coolbox group" :href="item.hrefLink" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex gap-2 px-4">
|
||||
<div class="pb-2 truncate box-title" x-text="item.name"></div>
|
||||
|
|
|
|||
|
|
@ -8,27 +8,27 @@
|
|||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item sm:min-w-fit" target="_blank" href="{{ $service->documentation() }}">Documentation
|
||||
<x-external-link /></a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">General</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.environment-variables', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Environment
|
||||
Variables</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.storages', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Persistent
|
||||
Storages</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Scheduled
|
||||
Tasks</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Webhooks</a>
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.resource-operations', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Resource
|
||||
Operations</a>
|
||||
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.tags', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Tags</a>
|
||||
|
||||
<a class='menu-item' wire:current.exact="menu-item-active"
|
||||
<a class='menu-item' wire:current.exact="menu-item-active" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.danger', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid]) }}">Danger
|
||||
Zone</a>
|
||||
</div>
|
||||
|
|
@ -104,7 +104,7 @@ class="w-4 h-4 dark:text-warning text-coollabs"
|
|||
<div class="pt-2 text-xs">{{ formatContainerStatus($application->status) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
<a class="mx-4 text-xs font-bold hover:underline"
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $application->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
|
|
@ -154,12 +154,12 @@ class="w-4 h-4 dark:text-warning text-coollabs"
|
|||
</div>
|
||||
<div class="flex items-center px-4">
|
||||
@if ($database->isBackupSolutionAvailable() || $database->is_migrated)
|
||||
<a class="mx-4 text-xs font-bold hover:underline"
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups">
|
||||
Backups
|
||||
</a>
|
||||
@endif
|
||||
<a class="mx-4 text-xs font-bold hover:underline"
|
||||
<a class="mx-4 text-xs font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||
<div class="navbar-main" x-data">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}"
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
@else
|
||||
<div class="flex flex-wrap order-first gap-2 items-center sm:order-last">
|
||||
<div class="text-error">
|
||||
Unable to deploy. <a class="underline font-bold cursor-pointer"
|
||||
Unable to deploy. <a class="underline font-bold cursor-pointer" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.environment-variables', $parameters) }}">
|
||||
Required environment variables missing.</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="flex flex-col items-start gap-2 min-w-fit">
|
||||
<a class="menu-item"
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
|
||||
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
|
||||
<button><- Back</button>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -5,17 +5,20 @@
|
|||
logsLoaded: false,
|
||||
fullscreen: false,
|
||||
alwaysScroll: false,
|
||||
intervalId: null,
|
||||
rafId: null,
|
||||
scrollDebounce: null,
|
||||
colorLogs: localStorage.getItem('coolify-color-logs') === 'true',
|
||||
searchQuery: '',
|
||||
renderTrigger: 0,
|
||||
matchCount: 0,
|
||||
containerName: '{{ $container ?? "logs" }}',
|
||||
makeFullscreen() {
|
||||
this.fullscreen = !this.fullscreen;
|
||||
if (this.fullscreen === false) {
|
||||
this.alwaysScroll = false;
|
||||
clearInterval(this.intervalId);
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
handleKeyDown(event) {
|
||||
|
|
@ -24,67 +27,72 @@
|
|||
}
|
||||
},
|
||||
isScrolling: false,
|
||||
scrollToBottom() {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
this.isScrolling = true;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
setTimeout(() => { this.isScrolling = false; }, 50);
|
||||
}
|
||||
},
|
||||
scheduleScroll() {
|
||||
if (!this.alwaysScroll) return;
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
this.scrollToBottom();
|
||||
if (this.alwaysScroll) {
|
||||
setTimeout(() => this.scheduleScroll(), 250);
|
||||
}
|
||||
});
|
||||
},
|
||||
toggleScroll() {
|
||||
this.alwaysScroll = !this.alwaysScroll;
|
||||
if (this.alwaysScroll) {
|
||||
this.intervalId = setInterval(() => {
|
||||
const logsContainer = document.getElementById('logsContainer');
|
||||
if (logsContainer) {
|
||||
this.isScrolling = true;
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
setTimeout(() => { this.isScrolling = false; }, 50);
|
||||
}
|
||||
}, 100);
|
||||
this.scheduleScroll();
|
||||
} else {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
handleScroll(event) {
|
||||
// Skip if follow logs is disabled or this is a programmatic scroll
|
||||
if (!this.alwaysScroll || this.isScrolling) return;
|
||||
|
||||
// Debounce scroll handling to avoid false positives from DOM mutations
|
||||
// when Livewire re-renders and adds new log lines
|
||||
clearTimeout(this.scrollDebounce);
|
||||
this.scrollDebounce = setTimeout(() => {
|
||||
const el = event.target;
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
// Use larger threshold (100px) to avoid accidental disables
|
||||
if (distanceFromBottom > 100) {
|
||||
this.alwaysScroll = false;
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
}
|
||||
}, 150);
|
||||
},
|
||||
toggleColorLogs() {
|
||||
this.colorLogs = !this.colorLogs;
|
||||
localStorage.setItem('coolify-color-logs', this.colorLogs);
|
||||
this.applyColorLogs();
|
||||
},
|
||||
getLogLevel(text) {
|
||||
const lowerText = text.toLowerCase();
|
||||
// Error detection (highest priority)
|
||||
if (/\b(error|err|failed|failure|exception|fatal|panic|critical)\b/.test(lowerText)) {
|
||||
return 'error';
|
||||
}
|
||||
// Warning detection
|
||||
if (/\b(warn|warning|wrn|caution)\b/.test(lowerText)) {
|
||||
return 'warning';
|
||||
}
|
||||
// Debug detection
|
||||
if (/\b(debug|dbg|trace|verbose)\b/.test(lowerText)) {
|
||||
return 'debug';
|
||||
}
|
||||
// Info detection
|
||||
if (/\b(info|inf|notice)\b/.test(lowerText)) {
|
||||
return 'info';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
matchesSearch(line) {
|
||||
if (!this.searchQuery.trim()) return true;
|
||||
return line.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
applyColorLogs() {
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return;
|
||||
const lines = logs.querySelectorAll('[data-log-line]');
|
||||
lines.forEach(line => {
|
||||
const content = (line.dataset.logContent || '').toLowerCase();
|
||||
line.classList.remove('log-error', 'log-warning', 'log-debug', 'log-info');
|
||||
if (!this.colorLogs) return;
|
||||
if (/\b(error|err|failed|failure|exception|fatal|panic|critical)\b/.test(content)) {
|
||||
line.classList.add('log-error');
|
||||
} else if (/\b(warn|warning|wrn|caution)\b/.test(content)) {
|
||||
line.classList.add('log-warning');
|
||||
} else if (/\b(debug|dbg|trace|verbose)\b/.test(content)) {
|
||||
line.classList.add('log-debug');
|
||||
} else if (/\b(info|inf|notice)\b/.test(content)) {
|
||||
line.classList.add('log-info');
|
||||
}
|
||||
});
|
||||
},
|
||||
hasActiveLogSelection() {
|
||||
const selection = window.getSelection();
|
||||
|
|
@ -93,79 +101,62 @@
|
|||
}
|
||||
const logsContainer = document.getElementById('logs');
|
||||
if (!logsContainer) return false;
|
||||
|
||||
// Check if selection is within the logs container
|
||||
const range = selection.getRangeAt(0);
|
||||
return logsContainer.contains(range.commonAncestorContainer);
|
||||
},
|
||||
decodeHtml(text) {
|
||||
// Decode HTML entities, handling double-encoding with max iteration limit to prevent DoS
|
||||
let decoded = text;
|
||||
let prev = '';
|
||||
let iterations = 0;
|
||||
const maxIterations = 3; // Prevent DoS from deeply nested HTML entities
|
||||
applySearch() {
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return;
|
||||
const lines = logs.querySelectorAll('[data-log-line]');
|
||||
const query = this.searchQuery.trim().toLowerCase();
|
||||
let count = 0;
|
||||
|
||||
while (decoded !== prev && iterations < maxIterations) {
|
||||
prev = decoded;
|
||||
const doc = new DOMParser().parseFromString(decoded, 'text/html');
|
||||
decoded = doc.documentElement.textContent;
|
||||
iterations++;
|
||||
}
|
||||
return decoded;
|
||||
},
|
||||
renderHighlightedLog(el, text) {
|
||||
// Skip re-render if user has text selected in logs (preserves copy ability)
|
||||
// But always render if the element is empty (initial render)
|
||||
if (el.textContent && this.hasActiveLogSelection()) {
|
||||
return;
|
||||
}
|
||||
lines.forEach(line => {
|
||||
const content = (line.dataset.logContent || '').toLowerCase();
|
||||
const textSpan = line.querySelector('[data-line-text]');
|
||||
const matches = !query || content.includes(query);
|
||||
|
||||
const decoded = this.decodeHtml(text);
|
||||
el.textContent = '';
|
||||
line.classList.toggle('hidden', !matches);
|
||||
if (matches && query) count++;
|
||||
|
||||
if (!this.searchQuery.trim()) {
|
||||
el.textContent = decoded;
|
||||
return;
|
||||
}
|
||||
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
const lowerText = decoded.toLowerCase();
|
||||
let lastIndex = 0;
|
||||
|
||||
let index = lowerText.indexOf(query, lastIndex);
|
||||
while (index !== -1) {
|
||||
// Add text before match
|
||||
if (index > lastIndex) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex, index)));
|
||||
// Update highlighting
|
||||
if (textSpan) {
|
||||
const originalText = textSpan.dataset.lineText || '';
|
||||
if (!query) {
|
||||
textSpan.textContent = originalText;
|
||||
} else if (matches) {
|
||||
this.highlightText(textSpan, originalText, query);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.matchCount = query ? count : 0;
|
||||
},
|
||||
highlightText(el, text, query) {
|
||||
// Skip if user has selection
|
||||
if (this.hasActiveLogSelection()) return;
|
||||
|
||||
el.textContent = '';
|
||||
const lowerText = text.toLowerCase();
|
||||
let lastIndex = 0;
|
||||
let index = lowerText.indexOf(query, lastIndex);
|
||||
|
||||
while (index !== -1) {
|
||||
if (index > lastIndex) {
|
||||
el.appendChild(document.createTextNode(text.substring(lastIndex, index)));
|
||||
}
|
||||
// Add highlighted match
|
||||
const mark = document.createElement('span');
|
||||
mark.className = 'log-highlight';
|
||||
mark.textContent = decoded.substring(index, index + this.searchQuery.length);
|
||||
mark.textContent = text.substring(index, index + query.length);
|
||||
el.appendChild(mark);
|
||||
|
||||
lastIndex = index + this.searchQuery.length;
|
||||
lastIndex = index + query.length;
|
||||
index = lowerText.indexOf(query, lastIndex);
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < decoded.length) {
|
||||
el.appendChild(document.createTextNode(decoded.substring(lastIndex)));
|
||||
if (lastIndex < text.length) {
|
||||
el.appendChild(document.createTextNode(text.substring(lastIndex)));
|
||||
}
|
||||
},
|
||||
getMatchCount() {
|
||||
if (!this.searchQuery.trim()) return 0;
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return 0;
|
||||
const lines = logs.querySelectorAll('[data-log-line]');
|
||||
let count = 0;
|
||||
lines.forEach(line => {
|
||||
if (line.textContent.toLowerCase().includes(this.searchQuery.toLowerCase())) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
},
|
||||
downloadLogs() {
|
||||
const logs = document.getElementById('logs');
|
||||
if (!logs) return;
|
||||
|
|
@ -191,17 +182,23 @@
|
|||
this.$wire.getLogs(true);
|
||||
this.logsLoaded = true;
|
||||
}
|
||||
// Prevent Livewire from morphing logs container when text is selected
|
||||
Livewire.hook('morph.updating', ({ el, component, toEl, skip }) => {
|
||||
if (el.id === 'logs' && this.hasActiveLogSelection()) {
|
||||
skip();
|
||||
}
|
||||
|
||||
// Watch search query changes
|
||||
this.$watch('searchQuery', () => {
|
||||
this.applySearch();
|
||||
});
|
||||
// Re-render logs after Livewire updates
|
||||
Livewire.hook('commit', ({ succeed }) => {
|
||||
succeed(() => {
|
||||
this.$nextTick(() => { this.renderTrigger++; });
|
||||
});
|
||||
|
||||
// Apply colors after Livewire updates
|
||||
Livewire.hook('morph.updated', ({ el }) => {
|
||||
if (el.id === 'logs') {
|
||||
this.$nextTick(() => {
|
||||
this.applyColorLogs();
|
||||
this.applySearch();
|
||||
if (this.alwaysScroll) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}" @keydown.window="handleKeyDown($event)">
|
||||
|
|
@ -242,7 +239,7 @@ class="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-gray-400 pointer-ev
|
|||
title="Number of Lines" {{ $streamLogs ? 'readonly' : '' }}
|
||||
class="input input-sm w-32 pl-11 text-center dark:bg-coolgray-300" />
|
||||
</form>
|
||||
<span x-show="searchQuery.trim()" x-text="getMatchCount() + ' matches'"
|
||||
<span x-show="searchQuery.trim()" x-text="matchCount + ' matches'"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -288,6 +285,21 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-
|
|||
</svg>
|
||||
@endif
|
||||
</button>
|
||||
<button
|
||||
x-on:click="
|
||||
$wire.copyLogs().then(logs => {
|
||||
navigator.clipboard.writeText(logs);
|
||||
Livewire.dispatch('success', ['Logs copied to clipboard.']);
|
||||
});
|
||||
"
|
||||
title="Copy Logs"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||||
</svg>
|
||||
</button>
|
||||
<button x-on:click="downloadLogs()" title="Download Logs"
|
||||
class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
|
|
@ -359,7 +371,7 @@ class="flex overflow-y-auto overflow-x-hidden flex-col px-4 py-2 w-full min-w-0
|
|||
Showing last {{ number_format($maxDisplayLines) }} of {{ number_format($totalLines) }} lines
|
||||
</div>
|
||||
@endif
|
||||
<div x-show="searchQuery.trim() && getMatchCount() === 0"
|
||||
<div x-show="searchQuery.trim() && matchCount === 0"
|
||||
class="text-gray-500 dark:text-gray-400 py-2">
|
||||
No matches found.
|
||||
</div>
|
||||
|
|
@ -383,23 +395,12 @@ class="text-gray-500 dark:text-gray-400 py-2">
|
|||
// Format: 2025-Dec-04 09:44:58.198879
|
||||
$timestamp = "{$year}-{$monthName}-{$day} {$time}.{$microseconds}";
|
||||
}
|
||||
|
||||
@endphp
|
||||
<div data-log-line data-log-content="{{ $line }}"
|
||||
x-effect="renderTrigger; searchQuery; $el.classList.toggle('hidden', !matchesSearch($el.dataset.logContent))"
|
||||
x-bind:class="{
|
||||
'bg-red-500/10 dark:bg-red-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'error',
|
||||
'bg-yellow-500/10 dark:bg-yellow-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'warning',
|
||||
'bg-purple-500/10 dark:bg-purple-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'debug',
|
||||
'bg-blue-500/10 dark:bg-blue-500/15': colorLogs && getLogLevel($el.dataset.logContent) === 'info',
|
||||
}"
|
||||
class="flex gap-2">
|
||||
<div data-log-line data-log-content="{{ $line }}" class="flex gap-2 log-line">
|
||||
@if ($timestamp && $showTimeStamps)
|
||||
<span class="shrink-0 text-gray-500">{{ $timestamp }}</span>
|
||||
@endif
|
||||
<span data-line-text="{{ $logContent }}"
|
||||
x-effect="renderTrigger; searchQuery; renderHighlightedLog($el, $el.dataset.lineText)"
|
||||
class="whitespace-pre-wrap break-all"></span>
|
||||
<span data-line-text="{{ $logContent }}" class="whitespace-pre-wrap break-all">{{ $logContent }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div class="alert alert-warning">Metrics are not available for Docker Compose applications yet!</div>
|
||||
@elseif(!$resource->destination->server->isMetricsEnabled())
|
||||
<div class="alert alert-warning">Metrics are only available for servers with Sentinel & Metrics enabled!</div>
|
||||
<div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}">Server settings</a> to enable it.</div>
|
||||
<div>Go to <a class="underline dark:text-white" href="{{ route('server.show', $resource->destination->server->uuid) }}" {{ wireNavigate() }}>Server settings</a> to enable it.</div>
|
||||
@else
|
||||
@if (!str($resource->status)->contains('running'))
|
||||
<div class="alert alert-warning">Metrics are only available when the application container is running!</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<div class="flex flex-col flex-wrap gap-2 pt-4">
|
||||
@forelse($resource->scheduled_tasks as $task)
|
||||
@if ($resource->type() == 'application')
|
||||
<a class="coolbox"
|
||||
<a class="coolbox" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
|
||||
<span class="flex flex-col">
|
||||
<span class="text-lg font-bold">{{ $task->name }}
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
</span>
|
||||
</a>
|
||||
@elseif ($resource->type() == 'service')
|
||||
<a class="coolbox"
|
||||
<a class="coolbox" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.scheduled-tasks', [...$parameters, 'task_uuid' => $task->uuid]) }}">
|
||||
<span class="flex flex-col">
|
||||
<span class="text-lg font-bold">{{ $task->name }}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
@forelse ($project->environments->sortBy('created_at') as $environment)
|
||||
<div class="gap-2 coolbox group">
|
||||
<div class="flex flex-1 mx-6">
|
||||
<a class="flex flex-col justify-center flex-1"
|
||||
<a class="flex flex-col justify-center flex-1" {{ wireNavigate() }}
|
||||
href="{{ route('project.resource.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}">
|
||||
<div class="font-bold dark:text-white"> {{ $environment->name }}</div>
|
||||
<div class="description">
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
</a>
|
||||
@can('update', $project)
|
||||
<div class="flex items-center justify-center gap-2 text-xs">
|
||||
<a class="font-bold hover:underline"
|
||||
<a class="font-bold hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.environment.edit', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<h2>API Tokens</h2>
|
||||
@if (!$isApiEnabled)
|
||||
<div>API is disabled. If you want to use the API, please enable it in the <a
|
||||
href="{{ route('settings.advanced') }}" class="underline dark:text-white">Settings</a> menu.</div>
|
||||
href="{{ route('settings.advanced') }}" class="underline dark:text-white" {{ wireNavigate() }}>Settings</a> menu.</div>
|
||||
@else
|
||||
<div>Tokens are created with the current team as scope.</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
@can('view', $key)
|
||||
{{-- Admin/Owner: Clickable link --}}
|
||||
<a class="coolbox group"
|
||||
href="{{ route('security.private-key.show', ['private_key_uuid' => data_get($key, 'uuid')]) }}">
|
||||
href="{{ route('security.private-key.show', ['private_key_uuid' => data_get($key, 'uuid')]) }}" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col justify-center mx-6">
|
||||
<div class="box-title">
|
||||
{{ data_get($key, 'name') }}
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@
|
|||
</div>
|
||||
@else
|
||||
<div>Metrics are disabled for this server. Enable them in <a class="underline dark:text-white"
|
||||
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}">General</a> settings.</div>
|
||||
href="{{ route('server.show', ['server_uuid' => $server->uuid]) }}" {{ wireNavigate() }}>General</a> settings.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@
|
|||
<h4 class="pt-4 pb-2">Available Destinations</h4>
|
||||
<div class="flex gap-2">
|
||||
@foreach ($server->standaloneDockers as $docker)
|
||||
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
|
||||
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button>
|
||||
</a>
|
||||
@endforeach
|
||||
@foreach ($server->swarmDockers as $docker)
|
||||
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}">
|
||||
<a href="{{ route('destination.show', ['destination_uuid' => data_get($docker, 'uuid')]) }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>{{ data_get($docker, 'network') }} </x-forms.button>
|
||||
</a>
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<div class="subtitle">All your servers are here.</div>
|
||||
<div class="grid gap-4 lg:grid-cols-2 -mt-1">
|
||||
@forelse ($servers as $server)
|
||||
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}"
|
||||
<a href="{{ route('server.show', ['server_uuid' => data_get($server, 'uuid')]) }}" {{ wireNavigate() }}
|
||||
@class([
|
||||
'gap-2 border cursor-pointer coolbox group',
|
||||
'border-red-500' =>
|
||||
|
|
|
|||
|
|
@ -65,14 +65,14 @@ class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
|||
class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap pt-2">
|
||||
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}" href="{{ route('server.show', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
]) }}" {{ wireNavigate() }}>
|
||||
Configuration
|
||||
</a>
|
||||
|
||||
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
|
||||
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }} flex items-center gap-1" href="{{ route('server.proxy', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
]) }}" {{ wireNavigate() }}>
|
||||
Proxy
|
||||
@if ($this->hasTraefikOutdated)
|
||||
<svg class="w-4 h-4 text-warning" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
@ -84,7 +84,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
|
|||
@endif
|
||||
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}" href="{{ route('server.resources', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
]) }}" {{ wireNavigate() }}>
|
||||
Resources
|
||||
</a>
|
||||
@can('canAccessTerminal')
|
||||
|
|
@ -97,7 +97,7 @@ class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar
|
|||
@can('update', $server)
|
||||
<a class="{{ request()->routeIs('server.security.patches') ? 'dark:text-white' : '' }}" href="{{ route('server.security.patches', [
|
||||
'server_uuid' => data_get($server, 'uuid'),
|
||||
]) }}">
|
||||
]) }}" {{ wireNavigate() }}>
|
||||
Security
|
||||
</a>
|
||||
@endcan
|
||||
|
|
|
|||
|
|
@ -32,8 +32,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if ($containers->count() > 0)
|
||||
@if ($activeTab === 'managed')
|
||||
@if ($activeTab === 'managed')
|
||||
@php
|
||||
$managedResources = $server->definedResources()->sortBy('name', SORT_NATURAL);
|
||||
@endphp
|
||||
@if ($managedResources->count() > 0)
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto">
|
||||
|
|
@ -59,7 +62,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($server->definedResources()->sortBy('name',SORT_NATURAL) as $resource)
|
||||
@foreach ($managedResources as $resource)
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ data_get($resource->project(), 'name') }}
|
||||
|
|
@ -68,7 +71,7 @@
|
|||
{{ data_get($resource, 'environment.name') }}
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap hover:underline">
|
||||
<a class=""
|
||||
<a class="" {{ wireNavigate() }}
|
||||
href="{{ $resource->link() }}">{{ $resource->name }}
|
||||
<x-internal-link /></a>
|
||||
</td>
|
||||
|
|
@ -83,8 +86,7 @@
|
|||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
@endforelse
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -92,7 +94,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@elseif ($activeTab === 'unmanaged')
|
||||
@else
|
||||
<div>No managed resources found.</div>
|
||||
@endif
|
||||
@elseif ($activeTab === 'unmanaged')
|
||||
@if (count($unmanagedContainers) > 0)
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto">
|
||||
|
|
@ -116,7 +122,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($containers->sortBy('name',SORT_NATURAL) as $resource)
|
||||
@foreach (collect($unmanagedContainers)->sortBy('name', SORT_NATURAL) as $resource)
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ data_get($resource, 'Names') }}
|
||||
|
|
@ -146,19 +152,15 @@
|
|||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
@endforelse
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
@if ($activeTab === 'managed')
|
||||
<div>No managed resources found.</div>
|
||||
@elseif ($activeTab === 'unmanaged')
|
||||
</div>
|
||||
@else
|
||||
<div>No unmanaged resources found.</div>
|
||||
@endif
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<span class="text-xs text-neutral-500">(experimental)</span>
|
||||
<x-helper
|
||||
helper="Only available for apt, dnf and zypper package managers atm, more coming
|
||||
soon.<br/>Status notifications sent every week.<br/>You can disable notifications in the <a class='dark:text-white underline' href='{{ route('notifications.email') }}'>notification settings</a>." />
|
||||
soon.<br/>Status notifications sent every week.<br/>You can disable notifications in the <a class='dark:text-white underline' href='{{ route('notifications.email') }}' {{ wireNavigate() }}>notification settings</a>." />
|
||||
@if (isDev())
|
||||
<x-forms.button type="button" wire:click="sendTestEmail">
|
||||
Send Test Email (dev only)</x-forms.button>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
Please validate your server to enable Instance Backup.
|
||||
</div>
|
||||
<a href="{{ route('server.show', [$server->uuid]) }}"
|
||||
class="text-black hover:text-gray-700 dark:text-white dark:hover:text-gray-200 underline">
|
||||
class="text-black hover:text-gray-700 dark:text-white dark:hover:text-gray-200 underline" {{ wireNavigate() }}>
|
||||
Go to Server Settings to Validate
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,9 +50,14 @@ class="flex flex-col h-full gap-8 sm:flex-row">
|
|||
environments!
|
||||
</x-callout>
|
||||
@endif
|
||||
<h4 class="pt-4">UI Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_wire_navigate_enabled" label="SPA Navigation"
|
||||
helper="Enable single-page application (SPA) style navigation with prefetching on hover. When enabled, page transitions are smoother without full page reloads and pages are prefetched when hovering over links. Disable if you experience navigation issues." />
|
||||
</div>
|
||||
<h4 class="pt-4">Confirmation Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id=" is_sponsorship_popup_enabled" label="Show Sponsorship Popup"
|
||||
<x-forms.checkbox instantSave id="is_sponsorship_popup_enabled" label="Show Sponsorship Popup"
|
||||
helper="Show monthly sponsorship reminders to support Coolify development. Disable to hide these messages permanently." />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
href="{{ route('shared-variables.environment.show', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
]) }}">
|
||||
]) }}" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col justify-center flex-1 mx-6 ">
|
||||
<div class="box-title"> {{ $environment->name }}</div>
|
||||
<div class="box-description">
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@
|
|||
<div class="subtitle">Set Team / Project / Environment wide variables.</div>
|
||||
|
||||
<div class="flex flex-col gap-2 -mt-1">
|
||||
<a class="coolbox group" href="{{ route('shared-variables.team.index') }}">
|
||||
<a class="coolbox group" href="{{ route('shared-variables.team.index') }}" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col justify-center mx-6">
|
||||
<div class="box-title">Team wide</div>
|
||||
<div class="box-description">Usable for all resources in a team.</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="coolbox group" href="{{ route('shared-variables.project.index') }}">
|
||||
<a class="coolbox group" href="{{ route('shared-variables.project.index') }}" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col justify-center mx-6">
|
||||
<div class="box-title">Project wide</div>
|
||||
<div class="box-description">Usable for all resources in a project.</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="coolbox group" href="{{ route('shared-variables.environment.index') }}">
|
||||
<a class="coolbox group" href="{{ route('shared-variables.environment.index') }}" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col justify-center mx-6">
|
||||
<div class="box-title">Environment wide</div>
|
||||
<div class="box-description">Usable for all resources in an environment.</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<div class="flex flex-col gap-2">
|
||||
@forelse ($projects as $project)
|
||||
<a class="coolbox group"
|
||||
href="{{ route('shared-variables.project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
|
||||
href="{{ route('shared-variables.project.show', ['project_uuid' => data_get($project, 'uuid')]) }}" {{ wireNavigate() }}>
|
||||
<div class="flex flex-col justify-center mx-6 ">
|
||||
<div class="box-title">{{ $project->name }}</div>
|
||||
<div class="box-description ">
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ class="bg-transparent border-transparent hover:bg-transparent hover:border-trans
|
|||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap"><a
|
||||
class=""
|
||||
{{ wireNavigate() }}
|
||||
href="{{ $resource->link() }}">{{ $resource->name }}
|
||||
<x-internal-link /></a>
|
||||
</td>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue