diff --git a/app/Filament/Resources/NodeResource.php b/app/Filament/Resources/NodeResource.php index 5a8e06d6b..3b0bb2e74 100644 --- a/app/Filament/Resources/NodeResource.php +++ b/app/Filament/Resources/NodeResource.php @@ -3,15 +3,12 @@ namespace App\Filament\Resources; use App\Filament\Resources\NodeResource\Pages; -use App\Filament\Resources\NodeResource\RelationManagers; use App\Models\Node; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\SoftDeletingScope; class NodeResource extends Resource { @@ -25,31 +22,8 @@ class NodeResource extends Resource { return $form ->schema([ - Forms\Components\TextInput::make('uuid') - ->label('UUID') - ->required() - ->maxLength(36), - Forms\Components\TextInput::make('public') - ->required() - ->numeric(), - Forms\Components\TextInput::make('name') - ->required() - ->maxLength(191), - Forms\Components\Textarea::make('description') - ->columnSpanFull(), - Forms\Components\TextInput::make('location_id') - ->required() - ->numeric(), - Forms\Components\TextInput::make('fqdn') - ->required() - ->maxLength(191), - Forms\Components\TextInput::make('scheme') - ->required() - ->maxLength(191) - ->default('https'), Forms\Components\Toggle::make('behind_proxy') - ->required(), - Forms\Components\Toggle::make('maintenance_mode') + ->helperText('If you are running the daemon behind a proxy such as Cloudflare, select this to have the daemon skip looking for certificates on boot.') ->required(), Forms\Components\TextInput::make('memory') ->required() @@ -67,23 +41,40 @@ class NodeResource extends Resource ->default(0), Forms\Components\TextInput::make('upload_size') ->required() - ->numeric() + ->integer() ->default(100), - Forms\Components\TextInput::make('daemon_token_id') - ->required() - ->maxLength(16), Forms\Components\TextInput::make('daemonListen') ->required() - ->numeric() + ->integer() + ->label('Daemon Port') ->default(8080), Forms\Components\TextInput::make('daemonSFTP') ->required() - ->numeric() + ->integer() + ->label('Daemon SFTP Port') ->default(2022), Forms\Components\TextInput::make('daemonBase') ->required() ->maxLength(191) ->default('/home/daemon-files'), + + Forms\Components\ToggleButtons::make('public') + ->label('Node Visibility') + ->inline() + ->default(true) + ->helperText('By setting a node to private you will be denying the ability to auto-deploy to this node.') + ->options([ + true => 'Public', + false => 'Private', + ]) + ->colors([ + true => 'warning', + false => 'danger', + ]) + ->icons([ + true => 'heroicon-m-eye', + false => 'heroicon-m-lock-closed', + ]), ]); } @@ -93,48 +84,39 @@ class NodeResource extends Resource ->columns([ Tables\Columns\TextColumn::make('uuid') ->label('UUID') - ->searchable(), - Tables\Columns\TextColumn::make('public') - ->numeric() - ->sortable(), + ->searchable() + ->hidden(), + Tables\Columns\IconColumn::make('health') + ->alignCenter() + ->state(fn (Node $node) => $node->systemInformation()['version'] ?? false) + ->tooltip(fn (Node $node) => $node->systemInformation()['version'] ?? $node->systemInformation()['exception'] ?? 'Not Connected') + ->trueIcon('heroicon-m-heart') + ->default(false), Tables\Columns\TextColumn::make('name') ->searchable(), - Tables\Columns\TextColumn::make('location_id') - ->numeric() - ->sortable(), Tables\Columns\TextColumn::make('fqdn') ->searchable(), - Tables\Columns\TextColumn::make('scheme') - ->searchable(), - Tables\Columns\IconColumn::make('behind_proxy') - ->boolean(), - Tables\Columns\IconColumn::make('maintenance_mode') - ->boolean(), Tables\Columns\TextColumn::make('memory') ->numeric() ->sortable(), - Tables\Columns\TextColumn::make('memory_overallocate') - ->numeric() - ->sortable(), Tables\Columns\TextColumn::make('disk') ->numeric() ->sortable(), - Tables\Columns\TextColumn::make('disk_overallocate') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('upload_size') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('daemon_token_id') - ->searchable(), - Tables\Columns\TextColumn::make('daemonListen') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('daemonSFTP') - ->numeric() - ->sortable(), Tables\Columns\TextColumn::make('daemonBase') ->searchable(), + Tables\Columns\IconColumn::make('scheme') + ->label('SSL') + ->trueIcon('heroicon-m-lock-closed') + ->falseIcon('heroicon-m-lock-open') + ->state(fn (Node $node) => $node->scheme === 'https'), + Tables\Columns\IconColumn::make('public') + ->trueIcon('heroicon-m-eye') + ->falseIcon('heroicon-m-eye-slash') + ->sortable(), + Tables\Columns\TextColumn::make('servers_count') + ->counts('servers') + ->label('Servers') + ->icon('heroicon-m-server-stack'), Tables\Columns\TextColumn::make('created_at') ->dateTime() ->sortable() diff --git a/app/Filament/Resources/NodeResource/Pages/CreateNode.php b/app/Filament/Resources/NodeResource/Pages/CreateNode.php index c7652c024..fcfaaf292 100644 --- a/app/Filament/Resources/NodeResource/Pages/CreateNode.php +++ b/app/Filament/Resources/NodeResource/Pages/CreateNode.php @@ -3,10 +3,142 @@ namespace App\Filament\Resources\NodeResource\Pages; use App\Filament\Resources\NodeResource; -use Filament\Actions; +use Filament\Forms; +use Filament\Notifications\Notification; use Filament\Resources\Pages\CreateRecord; class CreateNode extends CreateRecord { protected static string $resource = NodeResource::class; + + public function form(Forms\Form $form): Forms\Form + { + return $form + ->columns(2) + ->schema([ + Forms\Components\TextInput::make('fqdn') + ->label('Domain Name') + ->placeholder('node.example.com') + ->helperText('Node\'s Domain Name') + ->required() + ->autofocus() + ->columns(3) + ->live(debounce: 500) + ->hidden(fn (Forms\Get $get) => !$get('isHostname')) + ->disabled(fn (Forms\Get $get) => !$get('isHostname')) + ->afterStateUpdated(function (Forms\Set $set, ?string $state) { + $hasRecords = checkdnsrr("$state.", 'A'); + if (!$hasRecords) { + Notification::make() + ->title('Your hostname does not appear to have a valid A record.') + ->warning() + ->send(); + } + }) + ->maxLength(191), + + Forms\Components\TextInput::make('fqdn') + ->label('IP Address') + ->placeholder('127.0.0.1') + ->helperText('Node\'s IP Address') + ->required() + ->ipv4() + ->columns(3) + ->live(debounce: 500) + ->hidden(fn (Forms\Get $get) => $get('isHostname')) + ->disabled(fn (Forms\Get $get) => $get('isHostname')) + ->afterStateUpdated(function (Forms\Set $set, ?string $state) { + $isIp = filter_var($state, FILTER_VALIDATE_IP) !== false; + $isSecure = request()->isSecure(); + + if ($isIp && $isSecure) { + Notification::make() + ->title('You cannot use an IP Address because you have a secure connection to the panel currently.') + ->danger() + ->send(); + $set('name', $state); + } + }) + ->maxLength(191), + + Forms\Components\ToggleButtons::make('isHostname') + ->label('Address Type') + ->options([ + true => 'Hostname', + false => 'IP Address', + ]) + ->inline() + ->live() + ->afterStateUpdated(function () { + + }) + ->default(true), + + Forms\Components\TextInput::make('daemonListen') + ->columns(1) + ->label('Port') + ->helperText('If you will be running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.') + ->minValue(0) + ->maxValue(65536) + ->default(8080) + ->required() + ->integer(), + + Forms\Components\ToggleButtons::make('scheme') + ->label('Communicate over SSL') + ->required() + ->dehydrated() + ->inline() + // request()->isSecure() + ->helperText(function (Forms\Get $get) { + if (request()->isSecure()) { + return 'Your Panel is currently using secure connection therefore so must your Daemon. + This automatically disables using an IP Address for a FQDN.'; + } + + if (filter_var($get('fqdn'), FILTER_VALIDATE_IP) !== false) { + return 'An IP address cannot use SSL.'; + } + + return ''; + }) + // ->helperText(fn (Forms\Get $get) => filter_var($get('fqdn'), FILTER_VALIDATE_IP) !== false ? 'An IP address cannot use SSL.' : '') + ->disabled(function (Forms\Get $get, Forms\Set $set) { + $isIp = filter_var($get('fqdn'), FILTER_VALIDATE_IP) !== false; + $isSecure = request()->isSecure(); + + if ($isSecure) { + $set('scheme', 'https'); + + return true; + } + + if ($isIp) { + $set('scheme', 'http'); + + return true; + } + }) + ->options([ + 'http' => 'HTTP', + 'https' => 'SSL (HTTPS)', + ]) + ->colors([ + 'http' => 'warning', + 'https' => 'success', + ]) + ->icons([ + 'http' => 'heroicon-m-lock-open', + 'https' => 'heroicon-m-lock-closed', + ]) + ->default('http'), + Forms\Components\TextInput::make('name') + ->required() + ->columnSpanFull() + ->regex('/[a-zA-Z0-9_\.\- ]+/') + ->helperText('Character limits: [a-zA-Z0-9_.-] and [Space]') + ->maxLength(100), + Forms\Components\Textarea::make('description')->columnSpanFull()->rows(5), + ]); + } } diff --git a/app/Filament/Resources/NodeResource/Pages/EditNode.php b/app/Filament/Resources/NodeResource/Pages/EditNode.php index f6db410fb..7be05ce3c 100644 --- a/app/Filament/Resources/NodeResource/Pages/EditNode.php +++ b/app/Filament/Resources/NodeResource/Pages/EditNode.php @@ -4,12 +4,44 @@ namespace App\Filament\Resources\NodeResource\Pages; use App\Filament\Resources\NodeResource; use Filament\Actions; +use Filament\Forms; +use Filament\Forms\Components\Wizard; use Filament\Resources\Pages\EditRecord; class EditNode extends EditRecord { protected static string $resource = NodeResource::class; + public function form(Forms\Form $form): Forms\Form + { + return $form + ->schema([ + Wizard::make([ + Forms\Components\Wizard\Step::make('Basic') + ->description('') + ->schema((new CreateNode())->form($form)->getComponents()), + Forms\Components\Wizard\Step::make('Configuration') + ->description('') + ->schema([ + + ]), + ]) + ->columns(4) + ->persistStepInQueryString() + ->columnSpanFull() + // ->startOnStep($this->getStartStep()) + // ->cancelAction($this->getCancelFormAction()) + // ->submitAction($this->getSubmitFormAction()) + // ->skippable($this->hasSkippableSteps()), + ]); + } + + protected function getSteps(): array + { + return [ + ]; + } + protected function getHeaderActions(): array { return [ diff --git a/app/Models/Node.php b/app/Models/Node.php index d1e383be1..e70beddf2 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Repositories\Daemon\DaemonConfigurationRepository; +use Exception; use Illuminate\Support\Str; use Symfony\Component\Yaml\Yaml; use Illuminate\Notifications\Notifiable; @@ -79,9 +81,9 @@ class Node extends Model 'fqdn' => 'required|string', 'scheme' => 'required', 'behind_proxy' => 'boolean', - 'memory' => 'required|numeric|min:1', + 'memory' => 'required|numeric|min:0', 'memory_overallocate' => 'required|numeric|min:-1', - 'disk' => 'required|numeric|min:1', + 'disk' => 'required|numeric|min:0', 'disk_overallocate' => 'required|numeric|min:-1', 'daemonBase' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/', 'daemonSFTP' => 'required|numeric|between:1,65535', @@ -96,9 +98,11 @@ class Node extends Model protected $attributes = [ 'public' => true, 'behind_proxy' => false, + 'memory' => 0, 'memory_overallocate' => 0, + 'disk' => 0, 'disk_overallocate' => 0, - 'daemonBase' => '/var/lib/panel/volumes', + 'daemonBase' => '/var/lib/pelican/volumes', 'daemonSFTP' => 2022, 'daemonListen' => 8080, 'maintenance_mode' => false, @@ -117,6 +121,23 @@ class Node extends Model ]; } + public function getRouteKeyName(): string + { + return 'id'; + } + + + protected static function booted(): void + { + static::creating(function (self $node) { + $node->uuid = Str::uuid(); + $node->daemon_token = encrypt(Str::random(self::DAEMON_TOKEN_LENGTH)); + $node->daemon_token_id = Str::random(self::DAEMON_TOKEN_ID_LENGTH); + + return true; + }); + } + /** * Get the connection address to use when making calls to this node. */ @@ -240,4 +261,27 @@ class Node extends Model ]; })->values(); } + + public function systemInformation(): array + { + return once(function () { + try { + return resolve(DaemonConfigurationRepository::class) + ->setNode($this) + ->getSystemInformation(connectTimeout: 1); + } catch (Exception $exception) { + $message = str($exception->getMessage()); + + if ($message->startsWith('cURL error 6: Could not resolve host')) { + $message = str('Could not resolve host'); + } + + if ($message->startsWith('cURL error 28: Failed to connect to ')) { + $message = $message->after('cURL error 28: ')->before(' after '); + } + + return ['exception' => $message->toString()]; + } + }); + } }