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' => [
if (isset($input['frequency']) && $input['frequency'] == 'custom') {
$rules['custom'] = [
'required',
new CronRule,
],
])->validate();
}
];
}
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,9 +21,9 @@ public function create(Server $server, array $input): FirewallRule
'mask' => $input['mask'] ?? null,
]);
$server->firewall()
->handler()
->addRule(
/** @var Firewall $firewallHandler */
$firewallHandler = $server->firewall()->handler();
$firewallHandler->addRule(
$rule->type,
$rule->getRealProtocol(),
$rule->port,
@ -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");
}
}
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]));
}

154
composer.lock generated
View File

@ -128,16 +128,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.322.6",
"version": "3.322.8",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "ae7b0edab466c3440fe007c07cb62ae32a4dbfca"
"reference": "fb5099160e49b676277ae787ff721628e5e4dd5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ae7b0edab466c3440fe007c07cb62ae32a4dbfca",
"reference": "ae7b0edab466c3440fe007c07cb62ae32a4dbfca",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/fb5099160e49b676277ae787ff721628e5e4dd5a",
"reference": "fb5099160e49b676277ae787ff721628e5e4dd5a",
"shasum": ""
},
"require": {
@ -220,22 +220,22 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.322.6"
"source": "https://github.com/aws/aws-sdk-php/tree/3.322.8"
},
"time": "2024-09-26T18:12:45+00:00"
"time": "2024-09-30T19:09:25+00:00"
},
{
"name": "bacon/bacon-qr-code",
"version": "v3.0.0",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "510de6eca6248d77d31b339d62437cc995e2fb41"
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/510de6eca6248d77d31b339d62437cc995e2fb41",
"reference": "510de6eca6248d77d31b339d62437cc995e2fb41",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f9cc1f52b5a463062251d666761178dbdb6b544f",
"reference": "f9cc1f52b5a463062251d666761178dbdb6b544f",
"shasum": ""
},
"require": {
@ -274,9 +274,9 @@
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.0"
"source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.1"
},
"time": "2024-04-18T11:16:25+00:00"
"time": "2024-10-01T13:55:55+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
@ -1240,16 +1240,16 @@
},
{
"name": "filament/actions",
"version": "v3.2.114",
"version": "v3.2.115",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/actions.git",
"reference": "4cf93bf9ff04a76a9256ce6df88216583aeccb15"
"reference": "38c6eb00c7e3265907b37482c2dfd411c6f910c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/4cf93bf9ff04a76a9256ce6df88216583aeccb15",
"reference": "4cf93bf9ff04a76a9256ce6df88216583aeccb15",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/38c6eb00c7e3265907b37482c2dfd411c6f910c9",
"reference": "38c6eb00c7e3265907b37482c2dfd411c6f910c9",
"shasum": ""
},
"require": {
@ -1289,20 +1289,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2024-09-17T08:30:20+00:00"
"time": "2024-09-27T13:16:08+00:00"
},
{
"name": "filament/filament",
"version": "v3.2.114",
"version": "v3.2.115",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/panels.git",
"reference": "8d28c9756a341349a5d7a0694a66be7cc2c986c3"
"reference": "8d0f0e7101c14fe2f00490172452767f16b39f02"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/8d28c9756a341349a5d7a0694a66be7cc2c986c3",
"reference": "8d28c9756a341349a5d7a0694a66be7cc2c986c3",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/8d0f0e7101c14fe2f00490172452767f16b39f02",
"reference": "8d0f0e7101c14fe2f00490172452767f16b39f02",
"shasum": ""
},
"require": {
@ -1354,20 +1354,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2024-09-23T14:09:56+00:00"
"time": "2024-09-27T13:16:11+00:00"
},
{
"name": "filament/forms",
"version": "v3.2.114",
"version": "v3.2.115",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/forms.git",
"reference": "46a42dbc18f9273a3a59c54e94222fa62855c702"
"reference": "ffa33043ea0ee67a4eed58535687f87311e4256b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/46a42dbc18f9273a3a59c54e94222fa62855c702",
"reference": "46a42dbc18f9273a3a59c54e94222fa62855c702",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/ffa33043ea0ee67a4eed58535687f87311e4256b",
"reference": "ffa33043ea0ee67a4eed58535687f87311e4256b",
"shasum": ""
},
"require": {
@ -1410,20 +1410,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2024-09-17T08:30:15+00:00"
"time": "2024-09-27T13:16:04+00:00"
},
{
"name": "filament/infolists",
"version": "v3.2.114",
"version": "v3.2.115",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/infolists.git",
"reference": "dd6e2319aea92c5444c52792c750edfeb057f62a"
"reference": "d4d3030644e3617aed252a5df3c385145ada0ec6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/dd6e2319aea92c5444c52792c750edfeb057f62a",
"reference": "dd6e2319aea92c5444c52792c750edfeb057f62a",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/d4d3030644e3617aed252a5df3c385145ada0ec6",
"reference": "d4d3030644e3617aed252a5df3c385145ada0ec6",
"shasum": ""
},
"require": {
@ -1461,20 +1461,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2024-09-17T08:30:15+00:00"
"time": "2024-09-27T13:16:10+00:00"
},
{
"name": "filament/notifications",
"version": "v3.2.114",
"version": "v3.2.115",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/notifications.git",
"reference": "03ea56e0729c98c65831ab0215285a7cb1c4117f"
"reference": "0272612e1d54e0520f8717b24c71b9b70f198c8f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/03ea56e0729c98c65831ab0215285a7cb1c4117f",
"reference": "03ea56e0729c98c65831ab0215285a7cb1c4117f",
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/0272612e1d54e0520f8717b24c71b9b70f198c8f",
"reference": "0272612e1d54e0520f8717b24c71b9b70f198c8f",
"shasum": ""
},
"require": {
@ -1513,20 +1513,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2024-07-31T11:53:11+00:00"
"time": "2024-09-27T13:16:07+00:00"
},
{
"name": "filament/support",
"version": "v3.2.114",
"version": "v3.2.115",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/support.git",
"reference": "2183eb1149ef9ab742256155adf2afedda322e6d"
"reference": "6dba51efd6f2a32db21bc8684cd663915ab0e4d7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/support/zipball/2183eb1149ef9ab742256155adf2afedda322e6d",
"reference": "2183eb1149ef9ab742256155adf2afedda322e6d",
"url": "https://api.github.com/repos/filamentphp/support/zipball/6dba51efd6f2a32db21bc8684cd663915ab0e4d7",
"reference": "6dba51efd6f2a32db21bc8684cd663915ab0e4d7",
"shasum": ""
},
"require": {
@ -1572,20 +1572,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2024-09-23T14:10:13+00:00"
"time": "2024-09-27T13:16:20+00:00"
},
{
"name": "filament/tables",
"version": "v3.2.114",
"version": "v3.2.115",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/tables.git",
"reference": "75acf6f38a8ccfded57dc62bc3af0dd0bb04069d"
"reference": "07226fcd080f0f547aac31cf5117bfab192ea770"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/75acf6f38a8ccfded57dc62bc3af0dd0bb04069d",
"reference": "75acf6f38a8ccfded57dc62bc3af0dd0bb04069d",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/07226fcd080f0f547aac31cf5117bfab192ea770",
"reference": "07226fcd080f0f547aac31cf5117bfab192ea770",
"shasum": ""
},
"require": {
@ -1624,11 +1624,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2024-09-17T08:30:46+00:00"
"time": "2024-09-27T13:16:23+00:00"
},
{
"name": "filament/widgets",
"version": "v3.2.114",
"version": "v3.2.115",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/widgets.git",
@ -2343,16 +2343,16 @@
},
{
"name": "laravel/framework",
"version": "v11.25.0",
"version": "v11.26.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "b487a9089c0b1c71ac63bb6bc44fb4b00dc6da2e"
"reference": "b8cb8998701d5b3cfe68539d3c3da1fc59ddd82b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/b487a9089c0b1c71ac63bb6bc44fb4b00dc6da2e",
"reference": "b487a9089c0b1c71ac63bb6bc44fb4b00dc6da2e",
"url": "https://api.github.com/repos/laravel/framework/zipball/b8cb8998701d5b3cfe68539d3c3da1fc59ddd82b",
"reference": "b8cb8998701d5b3cfe68539d3c3da1fc59ddd82b",
"shasum": ""
},
"require": {
@ -2371,7 +2371,7 @@
"fruitcake/php-cors": "^1.3",
"guzzlehttp/guzzle": "^7.8",
"guzzlehttp/uri-template": "^1.0",
"laravel/prompts": "^0.1.18|^0.2.0",
"laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
"laravel/serializable-closure": "^1.3",
"league/commonmark": "^2.2.1",
"league/flysystem": "^3.8.0",
@ -2548,7 +2548,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2024-09-26T11:21:58+00:00"
"time": "2024-10-01T14:29:34+00:00"
},
{
"name": "laravel/prompts",
@ -3013,16 +3013,16 @@
},
{
"name": "league/flysystem",
"version": "3.28.0",
"version": "3.29.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c"
"reference": "0adc0d9a51852e170e0028a60bd271726626d3f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c",
"reference": "e611adab2b1ae2e3072fa72d62c62f52c2bf1f0c",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0adc0d9a51852e170e0028a60bd271726626d3f0",
"reference": "0adc0d9a51852e170e0028a60bd271726626d3f0",
"shasum": ""
},
"require": {
@ -3090,22 +3090,22 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/3.28.0"
"source": "https://github.com/thephpleague/flysystem/tree/3.29.0"
},
"time": "2024-05-22T10:09:12+00:00"
"time": "2024-09-29T11:59:11+00:00"
},
{
"name": "league/flysystem-local",
"version": "3.28.0",
"version": "3.29.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-local.git",
"reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40"
"reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/13f22ea8be526ea58c2ddff9e158ef7c296e4f40",
"reference": "13f22ea8be526ea58c2ddff9e158ef7c296e4f40",
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27",
"reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27",
"shasum": ""
},
"require": {
@ -3139,9 +3139,9 @@
"local"
],
"support": {
"source": "https://github.com/thephpleague/flysystem-local/tree/3.28.0"
"source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0"
},
"time": "2024-05-06T20:05:52+00:00"
"time": "2024-08-09T21:24:39+00:00"
},
{
"name": "league/mime-type-detection",
@ -3939,16 +3939,16 @@
},
{
"name": "nikic/php-parser",
"version": "v5.2.0",
"version": "v5.3.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb"
"reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb",
"reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a",
"reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a",
"shasum": ""
},
"require": {
@ -3991,9 +3991,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0"
},
"time": "2024-09-15T16:40:33+00:00"
"time": "2024-09-29T13:56:26+00:00"
},
{
"name": "nunomaduro/termwind",
@ -8511,16 +8511,16 @@
},
{
"name": "laravel/sail",
"version": "v1.33.0",
"version": "v1.34.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
"reference": "d54af9d5745e3680d8a6463ffd9f314aa53eb2d1"
"reference": "511e9c95b0f3ee778dc9e11e242bcd2af8e002cd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/d54af9d5745e3680d8a6463ffd9f314aa53eb2d1",
"reference": "d54af9d5745e3680d8a6463ffd9f314aa53eb2d1",
"url": "https://api.github.com/repos/laravel/sail/zipball/511e9c95b0f3ee778dc9e11e242bcd2af8e002cd",
"reference": "511e9c95b0f3ee778dc9e11e242bcd2af8e002cd",
"shasum": ""
},
"require": {
@ -8570,7 +8570,7 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
"time": "2024-09-22T19:04:21+00:00"
"time": "2024-09-27T14:58:09+00:00"
},
{
"name": "mockery/mockery",

View File

@ -211,4 +211,5 @@
// 'ExampleClass' => App\Example\ExampleClass::class,
])->toArray(),
'version' => '2.0.0',
];

View File

@ -172,6 +172,56 @@
'vito-agent' => \App\SSH\Services\Monitoring\VitoAgent\VitoAgent::class,
'remote-monitor' => \App\SSH\Services\Monitoring\RemoteMonitor\RemoteMonitor::class,
],
'service_versions' => [
'nginx' => [
'latest',
],
'mysql' => [
'5.7',
'8.0',
],
'mariadb' => [
'10.3',
'10.4',
'10.6',
'10.11',
'11.4',
],
'postgresql' => [
'12',
'13',
'14',
'15',
'16',
],
'redis' => [
'latest',
],
'php' => [
'5.6',
'7.0',
'7.1',
'7.2',
'7.3',
'7.4',
'8.0',
'8.1',
'8.2',
'8.3',
],
'ufw' => [
'latest',
],
'supervisor' => [
'latest',
],
'vito-agent' => [
'latest',
],
'remote-monitor' => [
'latest',
],
],
'service_units' => [
'nginx' => [
\App\Enums\OperatingSystem::UBUNTU20 => [

File diff suppressed because one or more lines are too long

View File

@ -13,7 +13,7 @@
filepond/dist/filepond.min.css:
(*!
* FilePond 4.31.3
* FilePond 4.31.4
* Licensed under MIT, https://opensource.org/licenses/MIT/
* Please visit https://pqina.nl/filepond/ for details.
*)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,20 @@ .choices__item--selectable {
@apply cursor-pointer;
}
.fi-sidebar {
@apply bg-gray-100/50 dark:bg-gray-900/50 !important;
}
.fi-sidebar-item-active a {
@apply bg-gray-100 dark:bg-gray-800/50 !important;
}
.fi-btn-color-primary {
background-image: linear-gradient(to bottom right, rgba(var(--primary-500), 1), rgba(var(--primary-900), 1));
box-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
}
.bg-primary-700-gradient {
background-image: linear-gradient(to bottom right, rgba(var(--primary-300), 1), rgba(var(--primary-700), 1));
box-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
}

View File

@ -0,0 +1,5 @@
<div class="p-6 text-sm">
<a href="https://github.com/vitodeploy/vito/releases/tag/{{ config("app.version") }}" target="_blank">
V{{ config("app.version") }}
</a>
</div>

View File

@ -1,3 +1,7 @@
<div {{ $getExtraAttributeBag() }}>
<div
@if (isset($getExtraAttributeBag))
{{ $getExtraAttributeBag() }}
@endif
>
{!! $content !!}
</div>

View File

@ -1,3 +1,6 @@
<form>
<div>
<form wire:submit="submit">
{{ $this->form }}
</form>
<x-filament-actions::modals />
</div>

View File

@ -1,3 +1,4 @@
<div>
{{ $this->infolist }}
<x-filament-actions::modals />
</div>

View File

@ -1,11 +1,13 @@
<div {{ $this->getExtraAttributesBag() }}>
<x-filament-panels::page>
@if (method_exists($this, "getSecondSubNavigation"))
<x-filament-panels::page.sub-navigation.tabs :navigation="$this->getSecondSubNavigation()" />
<x-filament-panels::page.sub-navigation.tabs class="!flex" :navigation="$this->getSecondSubNavigation()" />
@endif
@foreach ($this->getWidgets() as $key => $widget)
@livewire($widget[0], $widget[1] ?? [], key(class_basename($widget[0]) . "-" . $key))
@endforeach
<x-filament-actions::modals />
</x-filament-panels::page>
</div>

View File

@ -0,0 +1,53 @@
<x-filament::dropdown placement="bottom-start" :size="true" :teleport="true" class="pointer-choices -mx-2">
<x-slot name="trigger">
<button
@if (filament()->isSidebarCollapsibleOnDesktop())
x-data="{ tooltip: false }"
x-effect="
tooltip = $store.sidebar.isOpen
? false
: {
content: @js($currentProject->name),
placement: document.dir === 'rtl' ? 'left' : 'right',
theme: $store.theme,
}
"
x-tooltip.html="tooltip"
@endif
type="button"
class="fi-tenant-menu-trigger group flex w-full items-center justify-center gap-x-3 rounded-lg p-2 text-sm font-medium outline-none transition duration-75 hover:bg-gray-100 focus-visible:bg-gray-100 dark:hover:bg-white/5 dark:focus-visible:bg-white/5"
>
<div
class="bg-primary-700-gradient text-md flex size-8 items-center justify-center rounded-lg capitalize text-white"
>
{{ $currentProject->name[0] }}
</div>
<span
@if (filament()->isSidebarCollapsibleOnDesktop())
x-show="$store.sidebar.isOpen"
@endif
class="grid justify-items-start text-start"
>
<span class="text-gray-950 dark:text-white">
{{ $currentProject->name }}
</span>
</span>
<x-filament::icon
icon="heroicon-m-chevron-down"
icon-alias="panels::tenant-menu.toggle-button"
:x-show="filament()->isSidebarCollapsibleOnDesktop() ? '$store.sidebar.isOpen' : null"
class="ms-auto h-5 w-5 shrink-0 text-gray-400 transition duration-75 group-hover:text-gray-500 group-focus-visible:text-gray-500 dark:text-gray-500 dark:group-hover:text-gray-400 dark:group-focus-visible:text-gray-400"
/>
</button>
</x-slot>
<x-filament::dropdown.list>
@foreach ($projects as $project)
<x-filament::dropdown.list.item wire:click="updateProject({{ $project }})" class="cursor-pointer" tag="a">
{{ $project->name }}
</x-filament::dropdown.list.item>
@endforeach
</x-filament::dropdown.list>
</x-filament::dropdown>