2.x - firewall/metrics/services/cronjobs

This commit is contained in:
Saeed Vaziry
2024-10-01 19:09:38 +02:00
parent 2e9620409b
commit 906ddc38de
58 changed files with 1625 additions and 631 deletions

View File

@ -6,15 +6,11 @@
use App\Models\CronJob;
use App\Models\Server;
use App\ValidationRules\CronRule;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateCronJob
{
public function create(Server $server, array $input): void
{
$this->validate($input);
$cronJob = new CronJob([
'server_id' => $server->id,
'user' => $input['user'],
@ -29,12 +25,9 @@ public function create(Server $server, array $input): void
$cronJob->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
public static function rules(array $input): array
{
Validator::make($input, [
$rules = [
'command' => [
'required',
],
@ -46,15 +39,15 @@ private function validate(array $input): void
'required',
new CronRule(acceptCustom: true),
],
])->validate();
];
if ($input['frequency'] == 'custom') {
Validator::make($input, [
'custom' => [
'required',
new CronRule,
],
])->validate();
if (isset($input['frequency']) && $input['frequency'] == 'custom') {
$rules['custom'] = [
'required',
new CronRule,
];
}
return $rules;
}
}

View File

@ -5,15 +5,13 @@
use App\Enums\FirewallRuleStatus;
use App\Models\FirewallRule;
use App\Models\Server;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use App\SSH\Services\Firewall\Firewall;
use Illuminate\Validation\Rule;
class CreateRule
{
public function create(Server $server, array $input): FirewallRule
{
$this->validate($server, $input);
$rule = new FirewallRule([
'server_id' => $server->id,
'type' => $input['type'],
@ -23,15 +21,15 @@ public function create(Server $server, array $input): FirewallRule
'mask' => $input['mask'] ?? null,
]);
$server->firewall()
->handler()
->addRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
$rule->source,
$rule->mask
);
/** @var Firewall $firewallHandler */
$firewallHandler = $server->firewall()->handler();
$firewallHandler->addRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
$rule->source,
$rule->mask
);
$rule->status = FirewallRuleStatus::READY;
$rule->save();
@ -39,19 +37,16 @@ public function create(Server $server, array $input): FirewallRule
return $rule;
}
/**
* @throws ValidationException
*/
private function validate(Server $server, array $input): void
public static function rules(): array
{
Validator::make($input, [
return [
'type' => [
'required',
'in:allow,deny',
],
'protocol' => [
'required',
'in:'.implode(',', array_keys(config('core.firewall_protocols_port'))),
Rule::in(array_keys(config('core.firewall_protocols_port'))),
],
'port' => [
'required',
@ -64,8 +59,9 @@ private function validate(Server $server, array $input): void
'ip',
],
'mask' => [
'required',
'numeric',
],
])->validate();
];
}
}

View File

@ -5,13 +5,13 @@
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class GetMetrics
{
public function filter(Server $server, array $input): array
public function filter(Server $server, array $input): Collection
{
if (isset($input['from']) && isset($input['to']) && $input['from'] === $input['to']) {
$input['from'] = Carbon::parse($input['from'])->format('Y-m-d').' 00:00:00';
@ -24,8 +24,6 @@ public function filter(Server $server, array $input): array
$input = array_merge($defaultInput, $input);
$this->validate($input);
return $this->metrics(
server: $server,
fromDate: $this->getFromDate($input),
@ -39,8 +37,8 @@ private function metrics(
Carbon $fromDate,
Carbon $toDate,
?Expression $interval = null
): array {
$metrics = DB::table('metrics')
): Collection {
return DB::table('metrics')
->where('server_id', $server->id)
->whereBetween('created_at', [$fromDate->format('Y-m-d H:i:s'), $toDate->format('Y-m-d H:i:s')])
->select(
@ -64,10 +62,6 @@ private function metrics(
return $item;
});
return [
'metrics' => $metrics,
];
}
private function getFromDate(array $input): Carbon
@ -110,14 +104,12 @@ private function getInterval(array $input): Expression
return DB::raw("strftime('%Y-%m-%d %H:00:00', created_at) as date_interval");
}
if ($periodInHours > 24) {
return DB::raw("strftime('%Y-%m-%d 00:00:00', created_at) as date_interval");
}
return DB::raw("strftime('%Y-%m-%d 00:00:00', created_at) as date_interval");
}
private function validate(array $input): void
public static function rules(array $input): array
{
Validator::make($input, [
$rules = [
'period' => [
'required',
Rule::in([
@ -130,21 +122,13 @@ private function validate(array $input): void
'custom',
]),
],
])->validate();
];
if ($input['period'] === 'custom') {
Validator::make($input, [
'from' => [
'required',
'date',
'before:to',
],
'to' => [
'required',
'date',
'after:from',
],
])->validate();
if (isset($input['period']) && $input['period'] === 'custom') {
$rules['from'] = ['required', 'date', 'before:to'];
$rules['to'] = ['required', 'date', 'after:from'];
}
return $rules;
}
}

View File

@ -12,12 +12,10 @@ class Install
{
public function install(Server $server, array $input): Service
{
$this->validate($server, $input);
$service = new Service([
'server_id' => $server->id,
'name' => $input['name'],
'type' => $input['type'],
'type' => config('core.service_types')[$input['name']],
'version' => $input['version'],
'status' => ServiceStatus::INSTALLING,
]);
@ -40,18 +38,21 @@ public function install(Server $server, array $input): Service
return $service;
}
private function validate(Server $server, array $input): void
public static function rules(array $input): array
{
Validator::make($input, [
'type' => [
'required',
Rule::in(config('core.service_types')),
],
$rules = [
'name' => [
'required',
Rule::in(array_keys(config('core.service_types'))),
],
'version' => 'required',
])->validate();
'version' => [
'required',
],
];
if (isset($input['name'])) {
$rules['version'][] = Rule::in(config("core.service_versions.{$input['name']}"));
}
return $rules;
}
}

View File

@ -5,7 +5,6 @@
use App\Models\SshKey;
use App\Models\User;
use App\ValidationRules\SshKeyRule;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class CreateSshKey
@ -15,8 +14,6 @@ class CreateSshKey
*/
public function create(User $user, array $input): SshKey
{
$this->validate($input);
$key = new SshKey([
'user_id' => $user->id,
'name' => $input['name'],
@ -30,14 +27,14 @@ public function create(User $user, array $input): SshKey
/**
* @throws ValidationException
*/
private function validate(array $input): void
public static function rules(): array
{
Validator::make($input, [
return [
'name' => 'required',
'public_key' => [
'required',
new SshKeyRule,
],
])->validate();
];
}
}

View File

@ -6,17 +6,14 @@
use App\Models\Server;
use App\Models\SshKey;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class DeployKeyToServer
{
public function deploy(User $user, Server $server, array $input): void
public function deploy(Server $server, array $input): void
{
$this->validate($user, $input);
/** @var SshKey $sshKey */
$sshKey = SshKey::findOrFail($input['key_id']);
$sshKey = SshKey::query()->findOrFail($input['key_id']);
$server->sshKeys()->attach($sshKey, [
'status' => SshKeyStatus::ADDING,
]);
@ -26,13 +23,14 @@ public function deploy(User $user, Server $server, array $input): void
]);
}
private function validate(User $user, array $input): void
public static function rules(User $user, Server $server): array
{
Validator::make($input, [
return [
'key_id' => [
'required',
Rule::exists('ssh_keys', 'id')->where('user_id', $user->id),
Rule::unique('server_ssh_keys', 'ssh_key_id')->where('server_id', $server->id),
],
])->validate();
];
}
}

View File

@ -55,8 +55,6 @@ public static function rules(array $input): array
$rules = array_merge($rules, $provider->validationRules());
}
ds($rules);
return $rules;
}
}

View File

@ -5,6 +5,7 @@
use App\Exceptions\SSHAuthenticationError;
use App\Exceptions\SSHCommandError;
use App\Exceptions\SSHConnectionError;
use App\Exceptions\SSHError;
use App\Models\Server;
use App\Models\ServerLog;
use Exception;
@ -88,8 +89,7 @@ public function connect(bool $sftp = false): void
}
/**
* @throws SSHCommandError
* @throws SSHConnectionError
* @throws SSHError
*/
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false): string
{
@ -136,7 +136,6 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
return $output;
}
} catch (Throwable $e) {
throw $e;
throw new SSHCommandError($e->getMessage());
}
}

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -16,9 +17,15 @@
* @property float $disk_total
* @property float $disk_used
* @property float $disk_free
* @property-read float|int $memory_total_in_bytes
* @property-read float|int $memory_used_in_bytes
* @property-read float|int $memory_free_in_bytes
* @property-read float|int $disk_total_in_bytes
* @property-read float|int $disk_used_in_bytes
* @property-read float|int $disk_free_in_bytes
* @property Server $server
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class Metric extends Model
{
@ -50,4 +57,34 @@ public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
public function getMemoryTotalInBytesAttribute(): float|int
{
return $this->memory_total * 1024;
}
public function getMemoryUsedInBytesAttribute(): float|int
{
return $this->memory_used * 1024;
}
public function getMemoryFreeInBytesAttribute(): float|int
{
return $this->memory_free * 1024;
}
public function getDiskTotalInBytesAttribute(): float|int
{
return $this->disk_total * (1024 * 1024);
}
public function getDiskUsedInBytesAttribute(): float|int
{
return $this->disk_used * (1024 * 1024);
}
public function getDiskFreeInBytesAttribute(): float|int
{
return $this->disk_free * (1024 * 1024);
}
}

View File

@ -21,6 +21,7 @@
* @property string $status
* @property bool $is_default
* @property Server $server
* @property string $image_url
*/
class Service extends AbstractModel
{
@ -116,4 +117,9 @@ public function disable(): void
{
$this->unit && app(Manage::class)->disable($this);
}
public function getImageUrlAttribute(): string
{
return url('/static/images/'.$this->name.'.svg');
}
}

View File

@ -5,11 +5,8 @@
use App\Enums\UserRole;
use App\Traits\HasTimezoneTimestamps;
use Carbon\Carbon;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
@ -42,7 +39,7 @@
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class User extends Authenticatable implements HasTenants
class User extends Authenticatable
{
use HasFactory;
use HasTimezoneTimestamps;
@ -119,16 +116,6 @@ public function projects(): BelongsToMany
return $this->belongsToMany(Project::class, 'user_project')->withTimestamps();
}
public function getTenants(Panel $panel): Collection
{
return $this->projects;
}
public function canAccessTenant(Model $tenant): bool
{
return $this->projects()->whereKey($tenant)->exists();
}
public function currentProject(): HasOne
{
return $this->HasOne(Project::class, 'id', 'current_project_id');

View File

@ -0,0 +1,41 @@
<?php
namespace App\Policies;
use App\Models\CronJob;
use App\Models\Server;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class CronJobPolicy
{
use HandlesAuthorization;
public function viewAny(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
}
public function view(User $user, CronJob $cronjob): bool
{
return ($user->isAdmin() || $cronjob->server->project->users->contains($user)) &&
$cronjob->server->isReady();
}
public function create(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
}
public function update(User $user, CronJob $cronjob): bool
{
return ($user->isAdmin() || $cronjob->server->project->users->contains($user)) &&
$cronjob->server->isReady();
}
public function delete(User $user, CronJob $cronjob): bool
{
return ($user->isAdmin() || $cronjob->server->project->users->contains($user)) &&
$cronjob->server->isReady();
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Policies;
use App\Models\FirewallRule;
use App\Models\Server;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class FirewallRulePolicy
{
use HandlesAuthorization;
public function viewAny(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
}
public function view(User $user, FirewallRule $rule): bool
{
return ($user->isAdmin() || $rule->server->project->users->contains($user)) &&
$rule->server->isReady();
}
public function create(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
}
public function update(User $user, FirewallRule $rule): bool
{
return ($user->isAdmin() || $rule->server->project->users->contains($user)) &&
$rule->server->isReady();
}
public function delete(User $user, FirewallRule $rule): bool
{
return ($user->isAdmin() || $rule->server->project->users->contains($user)) &&
$rule->server->isReady();
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Policies;
use App\Models\Metric;
use App\Models\Server;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class MetricPolicy
{
use HandlesAuthorization;
public function viewAny(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->service('monitoring');
}
public function view(User $user, Metric $metric): bool
{
return ($user->isAdmin() || $metric->server->project->users->contains($user)) &&
$metric->server->service('monitoring');
}
public function create(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->service('monitoring');
}
public function update(User $user, Metric $metric): bool
{
return ($user->isAdmin() || $metric->server->project->users->contains($user)) &&
$metric->server->service('monitoring');
}
public function delete(User $user, Metric $metric): bool
{
return ($user->isAdmin() || $metric->server->project->users->contains($user)) &&
$metric->server->service('monitoring');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Policies;
use App\Enums\ServiceStatus;
use App\Models\Server;
use App\Models\Service;
use App\Models\User;
@ -35,4 +36,52 @@ public function delete(User $user, Service $service): bool
{
return ($user->isAdmin() || $service->server->project->users->contains($user)) && $service->server->isReady();
}
public function start(User $user, Service $service): bool
{
return ($user->isAdmin() || $service->server->project->users->contains($user)) &&
$service->server->isReady() &&
in_array($service->status, [
ServiceStatus::STOPPED,
ServiceStatus::FAILED,
]);
}
public function stop(User $user, Service $service): bool
{
return ($user->isAdmin() || $service->server->project->users->contains($user)) &&
$service->server->isReady() &&
in_array($service->status, [
ServiceStatus::READY,
ServiceStatus::FAILED,
]);
}
public function restart(User $user, Service $service): bool
{
return ($user->isAdmin() || $service->server->project->users->contains($user)) &&
$service->server->isReady() &&
in_array($service->status, [
ServiceStatus::READY,
ServiceStatus::FAILED,
ServiceStatus::STOPPED,
]);
}
public function enable(User $user, Service $service): bool
{
return ($user->isAdmin() || $service->server->project->users->contains($user)) &&
$service->server->isReady() &&
$service->status == ServiceStatus::DISABLED;
}
public function disable(User $user, Service $service): bool
{
return ($user->isAdmin() || $service->server->project->users->contains($user)) &&
$service->server->isReady() &&
in_array($service->status, [
ServiceStatus::READY,
ServiceStatus::STOPPED,
]);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Server;
use App\Models\SshKey;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class SshKeyPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, SshKey $sshKey): bool
{
return $user->id === $sshKey->user_id;
}
public function create(User $user): bool
{
return true;
}
public function update(User $user, SshKey $sshKey): bool
{
return $user->id === $sshKey->user_id;
}
public function delete(User $user, SshKey $sshKey): bool
{
return $user->id === $sshKey->user_id;
}
public function viewAnyServer(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
}
public function viewServer(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady();
}
public function createServer(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
}
public function updateServer(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady();
}
public function deleteServer(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) &&
$server->isReady();
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Laravel\Telescope\TelescopeApplicationServiceProvider;
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// Telescope::night();
$this->hideSensitiveRequestDetails();
$isLocal = $this->app->environment('local');
Telescope::filter(function (IncomingEntry $entry) use ($isLocal) {
return $isLocal ||
$entry->isReportableException() ||
$entry->isFailedRequest() ||
$entry->isFailedJob() ||
$entry->isScheduledTask() ||
$entry->hasMonitoredTag();
});
}
/**
* Prevent sensitive request details from being logged by Telescope.
*/
protected function hideSensitiveRequestDetails(): void
{
if ($this->app->environment('local')) {
return;
}
Telescope::hideRequestParameters(['_token']);
Telescope::hideRequestHeaders([
'cookie',
'x-csrf-token',
'x-xsrf-token',
]);
}
/**
* Register the Telescope gate.
*
* This gate determines who can access Telescope in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewTelescope', function ($user) {
return in_array($user->email, [
//
]);
});
}
}

View File

@ -39,6 +39,10 @@ public function boot(): void
PanelsRenderHook::SIDEBAR_NAV_START,
fn () => Livewire::mount(SelectProject::class)
);
FilamentView::registerRenderHook(
PanelsRenderHook::SIDEBAR_FOOTER,
fn () => view('web.components.app-version')
);
FilamentColor::register([
'slate' => Color::Slate,
'gray' => Color::Gray,
@ -96,6 +100,7 @@ public function panel(Panel $panel): Panel
->login()
->spa()
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
->sidebarCollapsibleOnDesktop()
->globalSearchFieldKeyBindingSuffix();
}
}

View File

@ -18,8 +18,10 @@ public function update(string $user, string $cron): void
if ! sudo -u $user crontab -l; then
echo 'VITO_SSH_ERROR' && exit 1
fi
echo 'cron updated!'
EOD;
$this->server->ssh()->exec($command);
$this->server->ssh()->exec($command, 'update-cron');
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Web\Pages\Servers;
use App\Models\Server;
use App\Web\Components\Page;
use Filament\Actions\Action;
class Create extends Page
{
protected static ?string $slug = 'servers/create';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Create Server';
public static function canAccess(): bool
{
return auth()->user()?->can('create', Server::class) ?? false;
}
protected function getExtraAttributes(): array
{
return [];
}
public function getWidgets(): array
{
return [
[Widgets\CreateServer::class],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/servers/create-server.html')
->openUrlInNewTab(),
];
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Web\Pages\Servers\CronJobs;
use App\Actions\CronJob\CreateCronJob;
use App\Models\CronJob;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/cronjobs';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Cron Jobs';
protected $listeners = ['$refresh'];
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [CronJob::class, static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\CronJobsList::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/servers/cronjobs.html')
->openUrlInNewTab(),
Action::make('create')
->authorize(fn () => auth()->user()?->can('create', [CronJob::class, $this->server]))
->icon('heroicon-o-plus')
->modalWidth(MaxWidth::ExtraLarge)
->form([
TextInput::make('command')
->rules(fn (callable $get) => CreateCronJob::rules($get())['command'])
->helperText(fn () => view('web.components.link', [
'href' => 'https://vitodeploy.com/servers/cronjobs.html',
'external' => true,
'text' => 'How the command should look like?',
])),
Select::make('user')
->rules(fn (callable $get) => CreateCronJob::rules($get())['user'])
->options([
'vito' => 'vito',
'root' => 'root',
]),
Select::make('frequency')
->options(config('core.cronjob_intervals'))
->reactive()
->rules(fn (callable $get) => CreateCronJob::rules($get())['frequency']),
TextInput::make('custom')
->label('Custom Frequency (Cron)')
->rules(fn (callable $get) => CreateCronJob::rules($get())['custom'])
->visible(fn (callable $get) => $get('frequency') === 'custom')
->placeholder('0 * * * *'),
])
->action(function (array $data) {
try {
app(CreateCronJob::class)->create($this->server, $data);
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
}),
];
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Web\Pages\Servers\CronJobs\Widgets;
use App\Actions\CronJob\DeleteCronJob;
use App\Models\CronJob;
use App\Models\Server;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class CronJobsList extends Widget
{
public Server $server;
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return CronJob::query()->where('server_id', $this->server->id);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('command')
->limit(40)
->tooltip(fn (CronJob $cronJob) => $cronJob->command)
->searchable()
->copyable(),
TextColumn::make('created_at')
->formatStateUsing(fn (CronJob $cronJob) => $cronJob->created_at_by_timezone)
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
->actions([
Action::make('delete')
->icon('heroicon-o-trash')
->tooltip('Delete')
->color('danger')
->hiddenLabel()
->requiresConfirmation()
->authorize(fn (CronJob $record) => auth()->user()->can('delete', $record))
->action(function (CronJob $record) {
try {
app(DeleteCronJob::class)->delete($this->server, $record);
} catch (\Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
}
$this->dispatch('$refresh');
}),
]);
}
}

View File

@ -62,7 +62,7 @@ public function getTable(): Table
->hiddenLabel()
->icon('heroicon-o-rectangle-stack')
->modalHeading('Backup Files')
->color('secondary')
->color('gray')
->tooltip('Show backup files')
->authorize(fn (Backup $record) => auth()->user()->can('viewAny', [BackupFile::class, $record]))
->modalContent(fn (Backup $record) => view('web.components.dynamic-widget', [

View File

@ -65,7 +65,7 @@ private function passwordAction(): Action
return Action::make('password')
->hiddenLabel()
->icon('heroicon-o-key')
->color('secondary')
->color('gray')
->modalHeading('Database user\'s password')
->modalWidth(MaxWidth::Large)
->tooltip('Show the password')

View File

@ -0,0 +1,95 @@
<?php
namespace App\Web\Pages\Servers\Firewall;
use App\Actions\FirewallRule\CreateRule;
use App\Models\FirewallRule;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/firewall';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Firewall';
protected $listeners = ['$refresh'];
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [FirewallRule::class, static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\RulesList::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/servers/firewall.html')
->openUrlInNewTab(),
Action::make('create')
->authorize(fn () => auth()->user()?->can('create', [FirewallRule::class, $this->server]))
->label('Create a Rule')
->icon('heroicon-o-plus')
->modalWidth(MaxWidth::Large)
->form([
Select::make('type')
->native(false)
->options([
'allow' => 'Allow',
'deny' => 'Deny',
])
->rules(CreateRule::rules()['type']),
Select::make('protocol')
->native(false)
->options([
'tcp' => 'TCP',
'udp' => 'UDP',
])
->rules(CreateRule::rules()['protocol']),
TextInput::make('port')
->rules(CreateRule::rules()['port']),
TextInput::make('source')
->rules(CreateRule::rules()['source']),
TextInput::make('mask')
->rules(CreateRule::rules()['mask']),
])
->action(function (array $data) {
try {
app(CreateRule::class)->create($this->server, $data);
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
}),
];
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Web\Pages\Servers\Firewall\Widgets;
use App\Actions\FirewallRule\DeleteRule;
use App\Models\FirewallRule;
use App\Models\Server;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class RulesList extends Widget
{
public Server $server;
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return FirewallRule::query()->where('server_id', $this->server->id);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('type')
->sortable()
->extraAttributes(['class' => 'uppercase'])
->color(fn (FirewallRule $record) => $record->type === 'allow' ? 'green' : 'red'),
TextColumn::make('protocol')
->sortable()
->extraAttributes(['class' => 'uppercase']),
TextColumn::make('port')
->sortable(),
TextColumn::make('source')
->sortable(),
TextColumn::make('mask')
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
->actions([
Action::make('delete')
->icon('heroicon-o-trash')
->tooltip('Delete')
->color('danger')
->hiddenLabel()
->requiresConfirmation()
->authorize(fn (FirewallRule $record) => auth()->user()->can('delete', $record))
->action(function (FirewallRule $record) {
try {
app(DeleteRule::class)->delete($this->server, $record);
} catch (\Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
}
$this->dispatch('$refresh');
}),
]);
}
}

View File

@ -52,6 +52,12 @@ protected function getHeaderActions(): array
]);
return [
\Filament\Actions\Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/servers/create-server.html')
->openUrlInNewTab(),
\Filament\Actions\Action::make('create')
->label('Create a Server')
->icon('heroicon-o-plus')
@ -96,6 +102,8 @@ protected function getHeaderActions(): array
)
->placeholder('Select profile')
->native(false)
->live()
->reactive()
->selectablePlaceholder(false)
->visible(fn ($get) => $get('provider') !== ServerProvider::CUSTOM),
Grid::make()
@ -110,7 +118,7 @@ protected function getHeaderActions(): array
return [];
}
return \App\Models\ServerProvider::regions($get('serer_provider'));
return \App\Models\ServerProvider::regions($get('server_provider'));
})
->loadingMessage('Loading regions...')
->disabled(fn ($get) => ! $get('server_provider'))
@ -219,6 +227,7 @@ protected function getHeaderActions(): array
),
]),
])
->modalSubmitActionLabel('Create')
->action(function ($input) {
$this->authorize('create', Server::class);

View File

@ -86,7 +86,7 @@ public function getTable(): Table
Action::make('download')
->hiddenLabel()
->tooltip('Download')
->color('secondary')
->color('gray')
->icon('heroicon-o-archive-box-arrow-down')
->authorize(fn ($record) => auth()->user()->can('view', $record))
->action(fn (ServerLog $record) => $record->download()),

View File

@ -0,0 +1,39 @@
<?php
namespace App\Web\Pages\Servers\Metrics;
use App\Models\Metric;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/metrics';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Metrics';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Metric::class, static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\FilterForm::class, ['server' => $this->server]],
[Widgets\MetricDetails::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
return [];
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Web\Pages\Servers\Metrics\Widgets;
use App\Actions\Monitoring\GetMetrics;
use App\Models\Server;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\ViewField;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Widgets\Widget;
class FilterForm extends Widget implements HasForms
{
use InteractsWithForms;
protected static string $view = 'web.components.form';
public ?array $data = [
'period' => '1h',
'from' => null,
'to' => null,
];
public function updated($name, $value): void
{
if ($value !== 'custom') {
$this->dispatch('updateFilters', filters: $this->data);
}
if ($value === 'custom' && $this->data['from'] && $this->data['to']) {
$this->dispatch('updateFilters', filters: $this->data);
}
}
public Server $server;
public function form(Form $form): Form
{
return $form
->schema([
Grid::make()
->columns(3)
->schema([
Select::make('period')
->live()
->reactive()
->options([
'10m' => '10 Minutes',
'30m' => '30 Minutes',
'1h' => '1 Hour',
'12h' => '12 Hours',
'1d' => '1 Day',
'7d' => '7 Days',
'custom' => 'Custom',
])
->rules(fn (Get $get) => GetMetrics::rules($get())['period']),
DatePicker::make('from')
->reactive()
->visible(fn (Get $get) => $get('period') === 'custom')
->maxDate(fn (Get $get) => now())
->rules(fn (Get $get) => GetMetrics::rules($get())['from']),
DatePicker::make('to')
->reactive()
->visible(fn (Get $get) => $get('period') === 'custom')
->minDate(fn (Get $get) => $get('from') ?: now())
->maxDate(now())
->rules(fn (Get $get) => GetMetrics::rules($get())['to']),
]),
ViewField::make('data')
->reactive()
->view('web.components.dynamic-widget', [
'widget' => Metrics::class,
'params' => [
'server' => $this->server,
'filters' => $this->data,
],
]),
])
->statePath('data');
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Web\Pages\Servers\Metrics\Widgets;
use App\Models\Metric;
use App\Models\Server;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Components\Grid;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Concerns\InteractsWithInfolists;
use Filament\Infolists\Contracts\HasInfolists;
use Filament\Infolists\Infolist;
use Filament\Widgets\Widget;
use Illuminate\Support\Number;
class MetricDetails extends Widget implements HasForms, HasInfolists
{
use InteractsWithForms;
use InteractsWithInfolists;
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
protected static string $view = 'web.components.infolist';
public Server $server;
public function infolist(Infolist $infolist): Infolist
{
return $infolist
->record($this->server->metrics()->latest()->first())
->schema([
Grid::make()
->schema([
Section::make()
->heading('Memory')
->description('More details on memory')
->columnSpan(1)
->schema([
TextEntry::make('memory_total')
->label('Total Memory')
->alignRight()
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->memory_total_in_bytes, 2))
->inlineLabel(),
TextEntry::make('memory_used')
->label('Used Memory')
->alignRight()
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->memory_used_in_bytes, 2))
->inlineLabel(),
TextEntry::make('memory_free')
->label('Free Memory')
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->memory_free_in_bytes, 2))
->alignRight()
->inlineLabel(),
]),
Section::make()
->heading('Disk')
->description('More details on disk')
->columnSpan(1)
->schema([
TextEntry::make('disk_total')
->label('Total Disk')
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->disk_total_in_bytes, 2))
->alignRight()
->inlineLabel(),
TextEntry::make('disk_used')
->label('Used Disk')
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->disk_used_in_bytes, 2))
->alignRight()
->inlineLabel(),
TextEntry::make('disk_free')
->label('Free Disk')
->formatStateUsing(fn (Metric $record) => Number::fileSize($record->disk_free_in_bytes, 2))
->alignRight()
->inlineLabel(),
]),
]),
]);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Web\Pages\Servers\Metrics\Widgets;
use App\Actions\Monitoring\GetMetrics;
use App\Models\Metric;
use App\Models\Server;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Support\Number;
use Livewire\Attributes\On;
class Metrics extends BaseWidget
{
public Server $server;
public array $filters = [];
protected static bool $isLazy = false;
#[On('updateFilters')]
public function updateFilters(array $filters): void
{
$this->filters = $filters;
}
protected function getStats(): array
{
/** @var Metric $lastMetric */
$lastMetric = $this->server
->metrics()
->latest()
->first();
$metrics = app(GetMetrics::class)->filter($this->server, $this->filters);
return [
Stat::make('CPU Load', $lastMetric?->load ?? 0)
->color('success')
->chart($metrics->pluck('load')->toArray()),
Stat::make('Memory Usage', Number::fileSize($lastMetric->memory_used_in_bytes, 2))
->color('warning')
->chart($metrics->pluck('memory_used')->toArray()),
Stat::make('Disk Usage', Number::fileSize($lastMetric->disk_used_in_bytes, 2))
->color('primary')
->chart($metrics->pluck('disk_used')->toArray()),
];
}
}

View File

@ -10,6 +10,7 @@
use App\Web\Traits\PageHasServer;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Support\Enums\IconPosition;
class Index extends Page
{
@ -57,7 +58,8 @@ protected function getHeaderActions(): array
ActionGroup::make($phps)
->authorize(fn () => auth()->user()?->can('create', [Service::class, $this->server]))
->label('Install PHP')
->icon('heroicon-o-plus')
->icon('heroicon-o-chevron-up-down')
->iconPosition(IconPosition::After)
->dropdownPlacement('bottom-end')
->color('primary')
->button(),

View File

@ -0,0 +1,96 @@
<?php
namespace App\Web\Pages\Servers\SSHKeys;
use App\Actions\SshKey\CreateSshKey;
use App\Actions\SshKey\DeployKeyToServer;
use App\Models\Server;
use App\Models\SshKey;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/ssh-keys';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'SSH Keys';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAnyServer', [SshKey::class, static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\SshKeysList::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('deploy')
->label('Deploy a Key')
->authorize(fn () => auth()->user()?->can('createServer', [SshKey::class, $this->server]))
->icon('heroicon-o-rocket-launch')
->modalWidth(MaxWidth::Large)
->form([
Select::make('type')
->options([
'existing' => 'An existing key',
'new' => 'A new key',
])
->reactive()
->default('existing'),
Select::make('key_id')
->label('Key')
->options(auth()->user()->sshKeys()->pluck('name', 'id')->toArray())
->visible(fn ($get) => $get('type') === 'existing')
->rules(DeployKeyToServer::rules(auth()->user(), $this->server)['key_id']),
TextInput::make('name')
->label('Name')
->visible(fn ($get) => $get('type') === 'new')
->rules(CreateSshKey::rules()['name']),
Textarea::make('public_key')
->label('Public Key')
->visible(fn ($get) => $get('type') === 'new')
->rules(CreateSshKey::rules()['public_key']),
])
->modalSubmitActionLabel('Deploy')
->action(function (array $data) {
$this->validate();
try {
if (! isset($data['key_id'])) {
$data['key_id'] = app(CreateSshKey::class)->create(auth()->user(), $data)->id;
}
app(DeployKeyToServer::class)->deploy($this->server, $data);
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
}),
];
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Web\Pages\Servers\SSHKeys\Widgets;
use App\Actions\SshKey\DeleteKeyFromServer;
use App\Models\Server;
use App\Models\SshKey;
use Exception;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget;
use Illuminate\Database\Eloquent\Builder;
class SshKeysList extends TableWidget
{
public Server $server;
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return SshKey::query()->whereHas(
'servers',
fn (Builder $query) => $query->where('server_id', $this->server->id)
);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('name')
->sortable()
->searchable(),
TextColumn::make('user.name')
->sortable()
->searchable(),
TextColumn::make('created_at')
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
->actions([
Action::make('delete')
->icon('heroicon-o-trash')
->tooltip('Delete')
->color('danger')
->hiddenLabel()
->requiresConfirmation()
->authorize(fn (SshKey $record) => auth()->user()->can('deleteServer', [SshKey::class, $this->server]))
->action(function (SshKey $record) {
try {
app(DeleteKeyFromServer::class)->delete($this->server, $record);
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
}
$this->dispatch('$refresh');
}),
]);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Web\Pages\Servers\Services;
use App\Actions\Service\Install;
use App\Models\Server;
use App\Models\Service;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/services';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Services';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Service::class, static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\ServicesList::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
$availableServices = [];
foreach (config('core.service_handlers') as $key => $addOn) {
if (! $this->server->services()->where('name', $key)->exists()) {
$availableServices[$key] = $key;
}
}
return [
Action::make('install')
->label('Install Service')
->icon('heroicon-o-archive-box-arrow-down')
->modalWidth(MaxWidth::Large)
->authorize(fn () => auth()->user()?->can('create', [Service::class, $this->server]))
->form([
Select::make('name')
->searchable()
->options($availableServices)
->reactive()
->rules(fn ($get) => Install::rules($get())['name']),
Select::make('version')
->options(function (callable $get) {
if (! $get('name')) {
return [];
}
return collect(config("core.service_versions.{$get('name')}"))
->mapWithKeys(fn ($version) => [$version => $version]);
})
->rules(fn ($get) => Install::rules($get())['version'])
->reactive(),
])
->action(function (array $data) {
$this->validate();
try {
app(Install::class)->install($this->server, $data);
$this->redirect(self::getUrl(['server' => $this->server]));
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
}),
];
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Web\Pages\Servers\Services\Widgets;
use App\Actions\Service\Manage;
use App\Actions\Service\Uninstall;
use App\Models\Server;
use App\Models\Service;
use App\Web\Pages\Servers\Services\Index;
use Exception;
use Filament\Notifications\Notification;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget;
use Illuminate\Database\Eloquent\Builder;
class ServicesList extends TableWidget
{
public Server $server;
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return Service::query()->where('server_id', $this->server->id);
}
protected static ?string $heading = 'Installed Services';
protected function getTableColumns(): array
{
return [
ImageColumn::make('image_url')
->label('Service')
->size(24),
TextColumn::make('name')
->sortable(),
TextColumn::make('version')
->sortable(),
TextColumn::make('status')
->label('Status')
->badge()
->color(fn (Service $service) => Service::$statusColors[$service->status])
->sortable(),
TextColumn::make('created_at')
->label('Installed At')
->formatStateUsing(fn ($record) => $record->created_at_by_timezone),
];
}
/**
* @throws Exception
*/
public function getTable(): Table
{
return $this->table
->actions([
ActionGroup::make([
$this->serviceAction('start'),
$this->serviceAction('stop'),
$this->serviceAction('restart'),
$this->serviceAction('disable'),
$this->serviceAction('enable'),
$this->uninstallAction(),
]),
]);
}
private function serviceAction(string $type): Action
{
return Action::make($type)
->authorize(fn (Service $service) => auth()->user()?->can($type, $service))
->label(ucfirst($type).' Service')
->action(function (Service $service) use ($type) {
try {
app(Manage::class)->$type($service);
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
});
}
private function uninstallAction(): Action
{
return Action::make('uninstall')
->authorize(fn (Service $service) => auth()->user()?->can('delete', $service))
->label('Uninstall Service')
->color('danger')
->requiresConfirmation()
->action(function (Service $service) {
try {
app(Uninstall::class)->uninstall($service);
$this->redirect(Index::getUrl(['server' => $this->server]));
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
});
}
}

View File

@ -5,7 +5,6 @@
use App\Models\Server;
use App\Models\Site;
use App\Web\Components\Page;
use App\Web\Pages\Servers\Sites\Widgets\SitesList;
use App\Web\Traits\PageHasServer;
use Filament\Actions\CreateAction;
@ -29,7 +28,7 @@ public static function canAccess(): bool
public function getWidgets(): array
{
return [
[SitesList::class, ['server' => $this->server]],
[Widgets\SitesList::class, ['server' => $this->server]],
];
}

View File

@ -1,260 +0,0 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Actions\Server\CreateServer as CreateServerAction;
use App\Enums\ServerProvider;
use App\Enums\ServerType;
use App\Enums\Webserver;
use App\Models\Server;
use App\Web\Fields\AlertField;
use App\Web\Fields\ProviderField;
use App\Web\Pages\Servers\View;
use App\Web\Pages\Settings\ServerProviders\Actions\Create;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Filament\Widgets\Widget;
use Throwable;
class CreateServer extends Widget implements HasForms
{
use InteractsWithForms;
protected static string $view = 'web.components.form';
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
public ?string $provider = ServerProvider::HETZNER;
public ?string $server_provider = '';
public ?string $region = '';
public ?string $plan = '';
public ?string $public_key = '';
public ?string $name = '';
public ?string $ip = '';
public ?string $port = '';
public ?string $os = '';
public ?string $type = ServerType::REGULAR;
public ?string $webserver = Webserver::NGINX;
public ?string $database = '';
public ?string $php = '';
public function form(Form $form): Form
{
$publicKey = __('servers.create.public_key_text', [
'public_key' => get_public_key_content(),
]);
return $form
->schema([
ProviderField::make('provider')
->label('Select a provider')
->default(ServerProvider::CUSTOM)
->live()
->reactive()
->afterStateUpdated(function (callable $set) {
$set('server_provider', null);
$set('region', null);
$set('plan', null);
})
->rules(fn ($get) => CreateServerAction::rules($this->all())['provider']),
AlertField::make('alert')
->warning()
->message(__('servers.create.public_key_warning'))
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
Select::make('server_provider')
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM)
->label('Server provider connection')
->rules(fn ($get) => CreateServerAction::rules($this->all())['server_provider'])
->options(function ($get) {
return \App\Models\ServerProvider::getByProjectId(auth()->user()->current_project_id)
->where('provider', $this->provider)
->pluck('profile', 'id');
})
->live()
->suffixAction(
Action::make('connect')
->form(Create::form())
->modalHeading('Connect to a new server provider')
->modalSubmitActionLabel('Connect')
->icon('heroicon-o-wifi')
->tooltip('Connect to a new server provider')
->modalWidth(MaxWidth::Medium)
->authorize(fn () => auth()->user()->can('create', \App\Models\ServerProvider::class))
// TODO: remove this after filament #14319 is fixed
->url(\App\Web\Pages\Settings\ServerProviders\Index::getUrl())
->action(fn (array $data) => Create::action($data))
)
->placeholder('Select profile')
->native(false)
->selectablePlaceholder(false)
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM),
Grid::make()
->schema([
Select::make('region')
->label('Region')
->rules(fn ($get) => CreateServerAction::rules($this->all())['region'])
->live()
->reactive()
->options(function () {
if (! $this->server_provider) {
return [];
}
return \App\Models\ServerProvider::regions($this->server_provider);
})
->loadingMessage('Loading regions...')
->disabled(fn ($get) => ! $this->server_provider)
->placeholder(fn ($get) => $this->server_provider ? 'Select region' : 'Select connection first')
->searchable(),
Select::make('plan')
->label('Plan')
->rules(fn ($get) => CreateServerAction::rules($this->all())['plan'])
->reactive()
->options(function () {
if (! $this->server_provider || ! $this->region) {
return [];
}
return \App\Models\ServerProvider::plans($this->server_provider, $this->region);
})
->loadingMessage('Loading plans...')
->disabled(fn ($get) => ! $this->region)
->placeholder(fn ($get) => $this->region ? 'Select plan' : 'Select plan first')
->searchable(),
])
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM),
TextInput::make('public_key')
->label('Public Key')
->default($publicKey)
->suffixAction(
Action::make('copy')
->icon('heroicon-o-clipboard-document-list')
->tooltip('Copy')
->action(function ($livewire, $state) {
$livewire->js(
'window.navigator.clipboard.writeText("'.$state.'");'
);
Notification::make()
->success()
->title('Copied!')
->send();
})
)
->helperText('Run this command on your server as root user')
->disabled()
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
TextInput::make('name')
->label('Name')
->rules(fn ($get) => CreateServerAction::rules($this->all())['name']),
Grid::make()
->schema([
TextInput::make('ip')
->label('SSH IP Address')
->rules(fn ($get) => CreateServerAction::rules($this->all())['ip']),
TextInput::make('port')
->label('SSH Port')
->rules(fn ($get) => CreateServerAction::rules($this->all())['port']),
])
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
Grid::make()
->schema([
Select::make('os')
->label('OS')
->native(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['os'])
->options(
collect(config('core.operating_systems'))
->mapWithKeys(fn ($value) => [$value => $value])
),
Select::make('type')
->label('Server Type')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['type'])
->options(
collect(config('core.server_types'))
->mapWithKeys(fn ($value) => [$value => $value])
)
->default(ServerType::REGULAR),
]),
Grid::make(3)
->schema([
Select::make('webserver')
->label('Webserver')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['webserver'] ?? [])
->options(
collect(config('core.webservers'))->mapWithKeys(fn ($value) => [$value => $value])
),
Select::make('database')
->label('Database')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['database'] ?? [])
->options(
collect(config('core.databases_name'))
->mapWithKeys(fn ($value, $key) => [
$key => $value.' '.config('core.databases_version')[$key],
])
),
Select::make('php')
->label('PHP')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['php'] ?? [])
->options(
collect(config('core.php_versions'))
->mapWithKeys(fn ($value) => [$value => $value])
),
]),
Actions::make([
Action::make('create')
->label('Create Server')
->button()
->action(fn () => $this->submit()),
]),
])
->columns(1);
}
public function submit(): void
{
$this->authorize('create', Server::class);
$this->validate();
try {
$server = app(CreateServerAction::class)->create(auth()->user(), $this->all()['data']);
$this->redirect(View::getUrl(['server' => $server]));
} catch (Throwable $e) {
Notification::make()
->title($e->getMessage())
->danger()
->send();
}
}
}

View File

@ -49,7 +49,7 @@ public function infolist(Infolist $infolist): Infolist
TextEntry::make('last_updated_check')
->label('Last Updated Check')
->inlineLabel()
->state(fn (Server $record) => $record->getDateTimeByTimezone($record->last_update_check) ?? '-')
->state(fn (Server $record) => $record->last_update_check?->ago())
->suffixAction(
Action::make('check-update')
->icon('heroicon-o-arrow-path')

View File

@ -18,7 +18,7 @@ protected function getStats(): array
if ($this->server->webserver()) {
$stats[] = Stat::make('Sites', $this->server->sites()->count())
->icon('heroicon-o-globe-alt');
->icon('heroicon-o-cursor-arrow-ripple');
}
if ($this->server->database()) {

View File

@ -3,23 +3,24 @@
namespace App\Web\Pages\Settings\Projects\Widgets;
use App\Models\Project;
use Filament\Forms\Components\Select;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Widgets\Widget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class SelectProject extends Widget implements HasForms
class SelectProject extends Widget
{
use InteractsWithForms;
protected static string $view = 'web.widgets.select-project';
protected static string $view = 'web.components.form';
public ?Project $currentProject;
public Collection $projects;
public int|string|null $project;
protected function getFormSchema(): array
public function mount(): void
{
$options = Project::query()
$this->currentProject = auth()->user()->currentProject;
$this->projects = Project::query()
->where(function (Builder $query) {
if (auth()->user()->isAdmin()) {
return;
@ -27,35 +28,14 @@ protected function getFormSchema(): array
$query->where('user_id', auth()->id())
->orWhereHas('users', fn ($query) => $query->where('user_id', auth()->id()));
})
->get()
->mapWithKeys(fn ($project) => [$project->id => $project->name])
->toArray();
return [
Select::make('project')
->name('project')
->model($this->project)
->label('Project')
->searchable()
->options($options)
->searchPrompt('Select a project...')
->extraAttributes(['class' => '-mx-2 pointer-choices'])
->selectablePlaceholder(false)
->live(),
];
->get();
}
public function updatedProject($value): void
public function updateProject(Project $project): void
{
$project = Project::query()->findOrFail($value);
$this->authorize('view', $project);
auth()->user()->update(['current_project_id' => $value]);
auth()->user()->update(['current_project_id' => $project->id]);
$this->redirect('/app');
}
public function mount(): void
{
$this->project = auth()->user()->current_project_id;
$this->redirect('/');
}
}

View File

@ -3,25 +3,24 @@
namespace App\Web\Traits;
use App\Models\Server;
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
use App\Web\Pages\Servers\Databases\Index as DatabasesIndex;
use App\Web\Pages\Servers\Firewall\Index as FirewallIndex;
use App\Web\Pages\Servers\Logs\Index as LogsIndex;
use App\Web\Pages\Servers\Metrics\Index as MetricsIndex;
use App\Web\Pages\Servers\PHP\Index as PHPIndex;
use App\Web\Pages\Servers\Services\Index as ServicesIndex;
use App\Web\Pages\Servers\Settings as ServerSettings;
use App\Web\Pages\Servers\Sites\Index as SitesIndex;
use App\Web\Pages\Servers\SshKeys\Index as SshKeysIndex;
use App\Web\Pages\Servers\View as ServerView;
use App\Web\Pages\Servers\Widgets\ServerSummary;
use Filament\Navigation\NavigationItem;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
trait PageHasServer
{
public function getTitle(): string|Htmlable
{
return static::$title.' - '.$this->server->name;
}
public function getSubNavigation(): array
{
$items = [];
@ -35,7 +34,7 @@ public function getSubNavigation(): array
if (SitesIndex::canAccess()) {
$items[] = NavigationItem::make(SitesIndex::getNavigationLabel())
->icon('heroicon-o-globe-alt')
->icon('heroicon-o-cursor-arrow-ripple')
->isActiveWhen(fn () => request()->routeIs(SitesIndex::getRouteName().'*'))
->url(SitesIndex::getUrl(parameters: ['server' => $this->server]));
}
@ -54,6 +53,41 @@ public function getSubNavigation(): array
->url(PHPIndex::getUrl(parameters: ['server' => $this->server]));
}
if (FirewallIndex::canAccess()) {
$items[] = NavigationItem::make(FirewallIndex::getNavigationLabel())
->icon('heroicon-o-fire')
->isActiveWhen(fn () => request()->routeIs(FirewallIndex::getRouteName().'*'))
->url(FirewallIndex::getUrl(parameters: ['server' => $this->server]));
}
if (CronJobsIndex::canAccess()) {
$items[] = NavigationItem::make(CronJobsIndex::getNavigationLabel())
->icon('heroicon-o-clock')
->isActiveWhen(fn () => request()->routeIs(CronJobsIndex::getRouteName().'*'))
->url(CronJobsIndex::getUrl(parameters: ['server' => $this->server]));
}
if (SshKeysIndex::canAccess()) {
$items[] = NavigationItem::make(SshKeysIndex::getNavigationLabel())
->icon('heroicon-o-key')
->isActiveWhen(fn () => request()->routeIs(SshKeysIndex::getRouteName().'*'))
->url(SshKeysIndex::getUrl(parameters: ['server' => $this->server]));
}
if (ServicesIndex::canAccess()) {
$items[] = NavigationItem::make(ServicesIndex::getNavigationLabel())
->icon('heroicon-o-cog-6-tooth')
->isActiveWhen(fn () => request()->routeIs(ServicesIndex::getRouteName().'*'))
->url(ServicesIndex::getUrl(parameters: ['server' => $this->server]));
}
if (MetricsIndex::canAccess()) {
$items[] = NavigationItem::make(MetricsIndex::getNavigationLabel())
->icon('heroicon-o-chart-bar')
->isActiveWhen(fn () => request()->routeIs(MetricsIndex::getRouteName().'*'))
->url(MetricsIndex::getUrl(parameters: ['server' => $this->server]));
}
if (LogsIndex::canAccess()) {
$items[] = NavigationItem::make(LogsIndex::getNavigationLabel())
->icon('heroicon-o-square-3-stack-3d')
@ -63,7 +97,7 @@ public function getSubNavigation(): array
if (ServerSettings::canAccess()) {
$items[] = NavigationItem::make(ServerSettings::getNavigationLabel())
->icon('heroicon-o-cog-6-tooth')
->icon('heroicon-o-wrench-screwdriver')
->isActiveWhen(fn () => request()->routeIs(ServerSettings::getRouteName().'*'))
->url(ServerSettings::getUrl(parameters: ['server' => $this->server]));
}