ui updates on server
This commit is contained in:
@@ -5,13 +5,13 @@ namespace App\Actions\Server;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class RemoveServer
|
||||
class DeleteServer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
StopSentinel::run($server);
|
||||
$server->delete();
|
||||
$server->forceDelete();
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,9 @@ class StartSentinel
|
||||
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history,
|
||||
];
|
||||
if (isDev()) {
|
||||
data_set($environments, 'DEBUG', 'true');
|
||||
// data_set($environments, 'DEBUG', 'true');
|
||||
$mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
|
||||
$image = 'sentinel';
|
||||
// $image = 'sentinel';
|
||||
}
|
||||
$docker_environments = '-e "' . implode('" -e "', array_map(fn($key, $value) => "$key=$value", array_keys($environments), $environments)) . '"';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Server\RemoveServer;
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use App\Actions\Server\ValidateServer;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
@@ -726,7 +726,8 @@ class ServersController extends Controller
|
||||
if ($server->definedResources()->count() > 0) {
|
||||
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
|
||||
}
|
||||
RemoveServer::dispatch($server);
|
||||
$server->delete();
|
||||
DeleteServer::dispatch($server);
|
||||
|
||||
return response()->json(['message' => 'Server deleted.']);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class Show extends Component
|
||||
return ! $alreadyAddedNetworks->contains('network', $network['Name']);
|
||||
});
|
||||
if ($this->networks->count() === 0) {
|
||||
$this->dispatch('success', 'No new networks found.');
|
||||
$this->dispatch('success', 'No new destinations found on this server.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
61
app/Livewire/Server/Advanced.php
Normal file
61
app/Livewire/Server/Advanced.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Advanced extends Component
|
||||
{
|
||||
public Server $server;
|
||||
protected $rules = [
|
||||
'server.settings.concurrent_builds' => 'required|integer|min:1',
|
||||
'server.settings.dynamic_timeout' => 'required|integer|min:1',
|
||||
'server.settings.force_docker_cleanup' => 'required|boolean',
|
||||
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
|
||||
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
|
||||
'server.settings.delete_unused_volumes' => 'boolean',
|
||||
'server.settings.delete_unused_networks' => 'boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
|
||||
'server.settings.concurrent_builds' => 'Concurrent Builds',
|
||||
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
|
||||
'server.settings.force_docker_cleanup' => 'Force Docker Cleanup',
|
||||
'server.settings.docker_cleanup_frequency' => 'Docker Cleanup Frequency',
|
||||
'server.settings.docker_cleanup_threshold' => 'Docker Cleanup Threshold',
|
||||
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
|
||||
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
|
||||
];
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
$this->server->settings->refresh();
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
if (empty($frequency) || ! validate_cron_expression($frequency)) {
|
||||
$this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
|
||||
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
|
||||
}
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.advanced');
|
||||
}
|
||||
}
|
||||
45
app/Livewire/Server/CloudflareTunnels.php
Normal file
45
app/Livewire/Server/CloudflareTunnels.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class CloudflareTunnels extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected $rules = [
|
||||
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
|
||||
];
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.cloudflare-tunnels');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\RemoveServer;
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -28,8 +28,8 @@ class Delete extends Component
|
||||
|
||||
return;
|
||||
}
|
||||
RemoveServer::run($this->server);
|
||||
|
||||
$this->server->delete();
|
||||
DeleteServer::dispatch($this->server);
|
||||
return redirect()->route('server.index');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -89,7 +89,7 @@ class Form extends Component
|
||||
'server.settings.sentinel_metrics_history_days' => 'Metrics History',
|
||||
'server.settings.sentinel_push_interval_seconds' => 'Push Interval',
|
||||
'server.settings.is_sentinel_enabled' => 'Server API',
|
||||
'server.settings.sentinel_custom_url' => 'Sentinel URL',
|
||||
'server.settings.sentinel_custom_url' => 'Coolify URL',
|
||||
'server.settings.server_timezone' => 'Server Timezone',
|
||||
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
|
||||
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
|
||||
@@ -106,7 +106,8 @@ class Form extends Component
|
||||
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
|
||||
}
|
||||
|
||||
public function checkSyncStatus(){
|
||||
public function checkSyncStatus()
|
||||
{
|
||||
$this->server->refresh();
|
||||
$this->server->settings->refresh();
|
||||
}
|
||||
@@ -114,9 +115,10 @@ class Form extends Component
|
||||
public function regenerateSentinelToken()
|
||||
{
|
||||
try {
|
||||
$this->server->generateSentinelToken();
|
||||
$this->server->settings->generateSentinelToken();
|
||||
$this->server->settings->refresh();
|
||||
$this->dispatch('success', 'Sentinel token regenerated. Please restart your Sentinel.');
|
||||
$this->restartSentinel(notification: false);
|
||||
$this->dispatch('success', 'Token regenerated & Sentinel restarted.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -152,18 +154,28 @@ class Form extends Component
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
|
||||
public function updatedServerSettingsIsSentinelEnabled($value){
|
||||
if($value === false){
|
||||
public function updatedServerSettingsIsSentinelEnabled($value)
|
||||
{
|
||||
$this->validate();
|
||||
$this->validate([
|
||||
'server.settings.sentinel_custom_url' => 'required|url',
|
||||
]);
|
||||
if ($value === false) {
|
||||
StopSentinel::dispatch($this->server);
|
||||
$this->server->settings->is_metrics_enabled = false;
|
||||
$this->server->settings->save();
|
||||
$this->server->sentinelHeartbeat(isReset: true);
|
||||
} else {
|
||||
StartSentinel::run($this->server);
|
||||
try {
|
||||
StartSentinel::run($this->server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedServerSettingsIsMetricsEnabled(){
|
||||
public function updatedServerSettingsIsMetricsEnabled()
|
||||
{
|
||||
$this->restartSentinel();
|
||||
}
|
||||
|
||||
@@ -171,6 +183,7 @@ class Form extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
refresh_server_connection($this->server->privateKey);
|
||||
$this->validateServer(false);
|
||||
|
||||
@@ -179,6 +192,14 @@ class Form extends Component
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
|
||||
// if ($this->server->isSentinelEnabled()) {
|
||||
// StartSentinel::run($this->server);
|
||||
// } else {
|
||||
// StopSentinel::run($this->server);
|
||||
// $this->server->settings->is_metrics_enabled = false;
|
||||
// $this->server->settings->save();
|
||||
// $this->server->sentinelHeartbeat(isReset: true);
|
||||
// }
|
||||
// if ($this->server->isSentinelEnabled()) {
|
||||
// PullSentinelImageJob::dispatchSync($this->server);
|
||||
// ray('Sentinel is enabled');
|
||||
@@ -196,16 +217,23 @@ class Form extends Component
|
||||
// $this->checkPortForServerApi();
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->server->settings->refresh();
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function restartSentinel()
|
||||
public function restartSentinel($notification = true)
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->validate([
|
||||
'server.settings.sentinel_custom_url' => 'required|url',
|
||||
]);
|
||||
$version = get_latest_sentinel_version();
|
||||
StartSentinel::run($this->server, $version, true);
|
||||
$this->dispatch('success', 'Sentinel started.');
|
||||
if ($notification) {
|
||||
$this->dispatch('success', 'Sentinel started.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -227,7 +255,7 @@ class Form extends Component
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
} else {
|
||||
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
|
||||
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: ' . $error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Modal extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@ class Resources extends Component
|
||||
|
||||
public $parameters = [];
|
||||
|
||||
public Collection $unmanagedContainers;
|
||||
public Collection $containers;
|
||||
|
||||
public $activeTab = 'managed';
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
@@ -50,14 +52,29 @@ class Resources extends Component
|
||||
public function refreshStatus()
|
||||
{
|
||||
$this->server->refresh();
|
||||
$this->loadUnmanagedContainers();
|
||||
if ($this->activeTab === 'managed') {
|
||||
$this->loadManagedContainers();
|
||||
} else {
|
||||
$this->loadUnmanagedContainers();
|
||||
}
|
||||
$this->dispatch('success', 'Resource statuses refreshed.');
|
||||
}
|
||||
|
||||
public function loadManagedContainers()
|
||||
{
|
||||
try {
|
||||
$this->activeTab = 'managed';
|
||||
$this->containers = $this->server->refresh()->definedResources();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadUnmanagedContainers()
|
||||
{
|
||||
$this->activeTab = 'unmanaged';
|
||||
try {
|
||||
$this->unmanagedContainers = $this->server->loadUnmanagedContainers();
|
||||
$this->containers = $this->server->loadUnmanagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -65,13 +82,14 @@ class Resources extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->unmanagedContainers = collect();
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -14,15 +13,29 @@ class ShowPrivateKey extends Component
|
||||
|
||||
public $parameters;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
}
|
||||
|
||||
public function setPrivateKey($privateKeyId)
|
||||
{
|
||||
$originalPrivateKeyId = $this->server->getOriginal('private_key_id');
|
||||
try {
|
||||
$privateKey = PrivateKey::findOrFail($privateKeyId);
|
||||
$this->server->update(['private_key_id' => $privateKey->id]);
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Private key updated successfully.');
|
||||
$this->server->update(['private_key_id' => $privateKeyId]);
|
||||
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Private key updated successfully.');
|
||||
} else {
|
||||
throw new \Exception('Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->server->update(['private_key_id' => $originalPrivateKeyId]);
|
||||
$this->server->validateConnection();
|
||||
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
|
||||
} finally {
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->server->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,18 +46,16 @@ class ShowPrivateKey extends Component
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Server is reachable.');
|
||||
} else {
|
||||
ray($error);
|
||||
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->server->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Enums\ProxyTypes;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -17,7 +18,6 @@ use OpenApi\Attributes as OA;
|
||||
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
|
||||
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
|
||||
use Spatie\Url\Url;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
#[OA\Schema(
|
||||
@@ -45,7 +45,7 @@ use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Server extends BaseModel
|
||||
{
|
||||
use SchemalessAttributesTrait;
|
||||
use SchemalessAttributesTrait,SoftDeletes;
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
@@ -97,7 +97,8 @@ class Server extends BaseModel
|
||||
}
|
||||
}
|
||||
});
|
||||
static::deleting(function ($server) {
|
||||
|
||||
static::forceDeleting(function ($server) {
|
||||
$server->destinations()->each(function ($destination) {
|
||||
$destination->delete();
|
||||
});
|
||||
@@ -527,34 +528,6 @@ $schema://$host {
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
}
|
||||
|
||||
public function generateSentinelUrl() {
|
||||
if ($this->isLocalhost()) {
|
||||
return 'http://host.docker.internal:8000';
|
||||
}
|
||||
$settings = InstanceSettings::get();
|
||||
if ($settings->fqdn) {
|
||||
return $settings->fqdn;
|
||||
}
|
||||
if ($settings->ipv4) {
|
||||
return $settings->ipv4 . ':8000';
|
||||
}
|
||||
if ($settings->ipv6) {
|
||||
return $settings->ipv6 . ':8000';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function generateSentinelToken()
|
||||
{
|
||||
$data = [
|
||||
'server_uuid' => $this->uuid,
|
||||
];
|
||||
$token = json_encode($data);
|
||||
$encrypted = encrypt($token);
|
||||
$this->settings->sentinel_token = $encrypted;
|
||||
$this->settings->save();
|
||||
|
||||
return $encrypted;
|
||||
}
|
||||
|
||||
public function sentinelHeartbeat(bool $isReset = false)
|
||||
{
|
||||
@@ -568,7 +541,7 @@ $schema://$host {
|
||||
|
||||
public function isSentinelEnabled()
|
||||
{
|
||||
return $this->isMetricsEnabled() || $this->isServerApiEnabled() || !$this->isBuildServer();
|
||||
return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && !$this->isBuildServer();
|
||||
}
|
||||
|
||||
public function isMetricsEnabled()
|
||||
|
||||
@@ -59,10 +59,59 @@ class ServerSetting extends Model
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($setting) {
|
||||
$setting->is_sentinel_enabled = true;
|
||||
try {
|
||||
if (str($setting->sentinel_token)->isEmpty()) {
|
||||
$setting->generateSentinelToken(save: false);
|
||||
}
|
||||
if (str($setting->sentinel_custom_url)->isEmpty()) {
|
||||
$url = $setting->generateSentinelUrl(save: false);
|
||||
if (str($url)->isEmpty()) {
|
||||
$setting->is_sentinel_enabled = false;
|
||||
} else {
|
||||
$setting->is_sentinel_enabled = true;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
loggy('Error creating server setting: ' . $e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function generateSentinelToken(bool $save = true)
|
||||
{
|
||||
$data = [
|
||||
'server_uuid' => $this->server->uuid,
|
||||
];
|
||||
$token = json_encode($data);
|
||||
$encrypted = encrypt($token);
|
||||
$this->sentinel_token = $encrypted;
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $encrypted;
|
||||
}
|
||||
|
||||
public function generateSentinelUrl(bool $save = true)
|
||||
{
|
||||
$domain = null;
|
||||
$settings = InstanceSettings::get();
|
||||
if ($this->server->isLocalhost()) {
|
||||
$domain = 'http://host.docker.internal:8000';
|
||||
} else if ($settings->fqdn) {
|
||||
$domain = $settings->fqdn;
|
||||
} else if ($settings->ipv4) {
|
||||
$domain = $settings->ipv4 . ':8000';
|
||||
} else if ($settings->ipv6) {
|
||||
$domain = $settings->ipv6 . ':8000';
|
||||
}
|
||||
$this->sentinel_custom_url = $domain;
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
return $domain;
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
|
||||
Reference in New Issue
Block a user