diff --git a/app/Filament/Resources/ApiKeyResource.php b/app/Filament/Resources/ApiKeyResource.php index d50c23cf7..5c34bff37 100644 --- a/app/Filament/Resources/ApiKeyResource.php +++ b/app/Filament/Resources/ApiKeyResource.php @@ -4,12 +4,8 @@ namespace App\Filament\Resources; use App\Filament\Resources\ApiKeyResource\Pages; use App\Models\ApiKey; -use Filament\Forms; -use Filament\Forms\Form; use Filament\Resources\Components\Tab; use Filament\Resources\Resource; -use Filament\Tables; -use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; class ApiKeyResource extends Resource @@ -38,145 +34,6 @@ class ApiKeyResource extends Resource return 'application'; } - public static function form(Form $form): Form - { - return $form - ->schema([ - Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)), - Forms\Components\Hidden::make('token')->default(encrypt(str_random(ApiKey::KEY_LENGTH))), - - Forms\Components\Select::make('user_id') - ->hidden() - ->searchable() - ->preload() - ->relationship('user', 'username') - ->default(auth()->user()->id) - ->required(), - - Forms\Components\Select::make('key_type') - ->options(function (ApiKey $apiKey) { - $originalOptions = [ - ApiKey::TYPE_NONE => 'None', - ApiKey::TYPE_ACCOUNT => 'Account', - ApiKey::TYPE_APPLICATION => 'Application', - ApiKey::TYPE_DAEMON_USER => 'Daemon User', - ApiKey::TYPE_DAEMON_APPLICATION => 'Daemon Application', - ]; - - return collect($originalOptions) - ->filter(fn ($value, $key) => $key <= ApiKey::TYPE_APPLICATION || $apiKey->key_type === $key) - ->all(); - }) - ->hidden() - ->selectablePlaceholder(false) - ->required() - ->default(ApiKey::TYPE_APPLICATION), - - Forms\Components\Fieldset::make('Permissions') - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - ]) - ->schema( - collect(ApiKey::RESOURCES)->map(fn ($resource) => Forms\Components\ToggleButtons::make("r_$resource") - ->label(str($resource)->replace('_', ' ')->title()) - ->options([ - 0 => 'None', - 1 => 'Read', - // 2 => 'Write', - 3 => 'Read & Write', - ]) - ->icons([ - 0 => 'tabler-book-off', - 1 => 'tabler-book', - 2 => 'tabler-writing', - 3 => 'tabler-writing', - ]) - ->colors([ - 0 => 'success', - 1 => 'warning', - 2 => 'danger', - 3 => 'danger', - ]) - ->inline() - ->required() - ->disabledOn('edit') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - ]) - ->default(0), - )->all(), - ), - - Forms\Components\TagsInput::make('allowed_ips') - ->placeholder('Example: 127.0.0.1 or 192.168.1.1') - ->label('Whitelisted IPv4 Addresses') - ->helperText('Press enter to add a new IP address or leave blank to allow any IP address') - ->columnSpanFull() - ->hidden() - ->default(null), - - Forms\Components\Textarea::make('memo') - ->required() - ->label('Description') - ->helperText(' - Once you have assigned permissions and created this set of credentials you will be unable to come back and edit it. - If you need to make changes down the road you will need to create a new set of credentials. - ') - ->columnSpanFull(), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->searchable(false) - ->columns([ - Tables\Columns\TextColumn::make('user.username') - ->searchable() - ->hidden() - ->sortable(), - - Tables\Columns\TextColumn::make('key') - ->copyable() - ->icon('tabler-clipboard-text') - ->state(fn (ApiKey $key) => $key->identifier . decrypt($key->token)), - - Tables\Columns\TextColumn::make('memo') - ->label('Description') - ->wrap() - ->limit(50), - - Tables\Columns\TextColumn::make('identifier') - ->hidden() - ->searchable(), - - Tables\Columns\TextColumn::make('last_used_at') - ->label('Last Used') - ->dateTime() - ->sortable(), - - Tables\Columns\TextColumn::make('created_at') - ->label('Created') - ->dateTime() - ->sortable(), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - public static function getRelations(): array { return [ diff --git a/app/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php b/app/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php index 6dfaf6335..d39f63c4d 100644 --- a/app/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php +++ b/app/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php @@ -3,11 +3,106 @@ namespace App\Filament\Resources\ApiKeyResource\Pages; use App\Filament\Resources\ApiKeyResource; +use App\Models\ApiKey; +use Filament\Forms\Form; use Filament\Resources\Pages\CreateRecord; +use Filament\Forms; class CreateApiKey extends CreateRecord { protected static string $resource = ApiKeyResource::class; protected ?string $heading = 'Create Application API Key'; + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)), + Forms\Components\Hidden::make('token')->default(encrypt(str_random(ApiKey::KEY_LENGTH))), + + Forms\Components\Select::make('user_id') + ->hidden() + ->searchable() + ->preload() + ->relationship('user', 'username') + ->default(auth()->user()->id) + ->required(), + + Forms\Components\Select::make('key_type') + ->options(function (ApiKey $apiKey) { + $originalOptions = [ + ApiKey::TYPE_NONE => 'None', + ApiKey::TYPE_ACCOUNT => 'Account', + ApiKey::TYPE_APPLICATION => 'Application', + ApiKey::TYPE_DAEMON_USER => 'Daemon User', + ApiKey::TYPE_DAEMON_APPLICATION => 'Daemon Application', + ]; + + return collect($originalOptions) + ->filter(fn ($value, $key) => $key <= ApiKey::TYPE_APPLICATION || $apiKey->key_type === $key) + ->all(); + }) + ->hidden() + ->selectablePlaceholder(false) + ->required() + ->default(ApiKey::TYPE_APPLICATION), + + Forms\Components\Fieldset::make('Permissions') + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + ]) + ->schema( + collect(ApiKey::RESOURCES)->map(fn ($resource) => Forms\Components\ToggleButtons::make("r_$resource") + ->label(str($resource)->replace('_', ' ')->title()) + ->options([ + 0 => 'None', + 1 => 'Read', + // 2 => 'Write', + 3 => 'Read & Write', + ]) + ->icons([ + 0 => 'tabler-book-off', + 1 => 'tabler-book', + 2 => 'tabler-writing', + 3 => 'tabler-writing', + ]) + ->colors([ + 0 => 'success', + 1 => 'warning', + 2 => 'danger', + 3 => 'danger', + ]) + ->inline() + ->required() + ->disabledOn('edit') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + ]) + ->default(0), + )->all(), + ), + + Forms\Components\TagsInput::make('allowed_ips') + ->placeholder('Example: 127.0.0.1 or 192.168.1.1') + ->label('Whitelisted IPv4 Addresses') + ->helperText('Press enter to add a new IP address or leave blank to allow any IP address') + ->columnSpanFull() + ->hidden() + ->default(null), + + Forms\Components\Textarea::make('memo') + ->required() + ->label('Description') + ->helperText(' + Once you have assigned permissions and created this set of credentials you will be unable to come back and edit it. + If you need to make changes down the road you will need to create a new set of credentials. + ') + ->columnSpanFull(), + ]); + } } diff --git a/app/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php b/app/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php index 61ba51100..3d5c4b9a0 100644 --- a/app/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php +++ b/app/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php @@ -7,12 +7,61 @@ use App\Models\ApiKey; use Filament\Actions; use Filament\Resources\Components\Tab; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; +use Filament\Tables; class ListApiKeys extends ListRecords { protected static string $resource = ApiKeyResource::class; + public function table(Table $table): Table + { + return $table + ->searchable(false) + ->columns([ + Tables\Columns\TextColumn::make('user.username') + ->searchable() + ->hidden() + ->sortable(), + + Tables\Columns\TextColumn::make('key') + ->copyable() + ->icon('tabler-clipboard-text') + ->state(fn (ApiKey $key) => $key->identifier . decrypt($key->token)), + + Tables\Columns\TextColumn::make('memo') + ->label('Description') + ->wrap() + ->limit(50), + + Tables\Columns\TextColumn::make('identifier') + ->hidden() + ->searchable(), + + Tables\Columns\TextColumn::make('last_used_at') + ->label('Last Used') + ->dateTime() + ->sortable(), + + Tables\Columns\TextColumn::make('created_at') + ->label('Created') + ->dateTime() + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/DatabaseHostResource.php b/app/Filament/Resources/DatabaseHostResource.php index cc53f6b4a..528c4c6d3 100644 --- a/app/Filament/Resources/DatabaseHostResource.php +++ b/app/Filament/Resources/DatabaseHostResource.php @@ -4,12 +4,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\DatabaseHostResource\Pages; use App\Models\DatabaseHost; -use Filament\Forms; -use Filament\Forms\Components\Section; -use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; -use Filament\Tables\Table; class DatabaseHostResource extends Resource { @@ -19,85 +14,6 @@ class DatabaseHostResource extends Resource protected static ?string $navigationIcon = 'tabler-database'; - public static function form(Form $form): Form - { - return $form - ->schema([ - Section::make()->schema([ - Forms\Components\TextInput::make('host') - ->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.') - ->required() - ->live() - ->debounce(500) - ->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state)) - ->maxLength(191), - Forms\Components\TextInput::make('port') - ->helperText('The port that MySQL is running on for this host.') - ->required() - ->numeric() - ->default(3306) - ->minValue(0) - ->maxValue(65535), - Forms\Components\TextInput::make('username') - ->helperText('The username of an account that has enough permissions to create new users and databases on the system.') - ->required() - ->maxLength(191), - Forms\Components\TextInput::make('password') - ->helperText('The password for the database user.') - ->password() - ->revealable() - ->maxLength(191) - ->required(), - Forms\Components\TextInput::make('name') - ->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.') - ->required() - ->maxLength(60), - Forms\Components\Select::make('node_id') - ->searchable() - ->preload() - ->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.') - ->label('Linked Node') - ->relationship('node', 'name'), - ])->columns([ - 'default' => 1, - 'lg' => 2, - ]), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->searchable(false) - ->columns([ - Tables\Columns\TextColumn::make('name') - ->searchable(), - Tables\Columns\TextColumn::make('host') - ->searchable(), - Tables\Columns\TextColumn::make('port') - ->sortable(), - Tables\Columns\TextColumn::make('username') - ->searchable(), - Tables\Columns\TextColumn::make('max_databases') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('node.name') - ->numeric() - ->sortable(), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - public static function getRelations(): array { return [ diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php b/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php index b5dbf3e5b..ed8b2fa93 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php @@ -4,6 +4,9 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages; use App\Filament\Resources\DatabaseHostResource; use Filament\Resources\Pages\CreateRecord; +use Filament\Forms; +use Filament\Forms\Components\Section; +use Filament\Forms\Form; class CreateDatabaseHost extends CreateRecord { @@ -12,4 +15,50 @@ class CreateDatabaseHost extends CreateRecord protected ?string $heading = 'Database Hosts'; protected ?string $subheading = '(database servers that can have individual databases)'; + + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make()->schema([ + Forms\Components\TextInput::make('host') + ->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.') + ->required() + ->live() + ->debounce(500) + ->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state)) + ->maxLength(191), + Forms\Components\TextInput::make('port') + ->helperText('The port that MySQL is running on for this host.') + ->required() + ->numeric() + ->default(3306) + ->minValue(0) + ->maxValue(65535), + Forms\Components\TextInput::make('username') + ->helperText('The username of an account that has enough permissions to create new users and databases on the system.') + ->required() + ->maxLength(191), + Forms\Components\TextInput::make('password') + ->helperText('The password for the database user.') + ->password() + ->revealable() + ->maxLength(191) + ->required(), + Forms\Components\TextInput::make('name') + ->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.') + ->required() + ->maxLength(60), + Forms\Components\Select::make('node_id') + ->searchable() + ->preload() + ->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.') + ->label('Linked Node') + ->relationship('node', 'name'), + ])->columns([ + 'default' => 1, + 'lg' => 2, + ]), + ]); + } } diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php index 9860d636f..1fa7deb05 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php @@ -5,11 +5,60 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages; use App\Filament\Resources\DatabaseHostResource; use Filament\Actions; use Filament\Resources\Pages\EditRecord; +use Filament\Forms; +use Filament\Forms\Components\Section; +use Filament\Forms\Form; class EditDatabaseHost extends EditRecord { protected static string $resource = DatabaseHostResource::class; + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make()->schema([ + Forms\Components\TextInput::make('host') + ->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.') + ->required() + ->live() + ->debounce(500) + ->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state)) + ->maxLength(191), + Forms\Components\TextInput::make('port') + ->helperText('The port that MySQL is running on for this host.') + ->required() + ->numeric() + ->default(3306) + ->minValue(0) + ->maxValue(65535), + Forms\Components\TextInput::make('username') + ->helperText('The username of an account that has enough permissions to create new users and databases on the system.') + ->required() + ->maxLength(191), + Forms\Components\TextInput::make('password') + ->helperText('The password for the database user.') + ->password() + ->revealable() + ->maxLength(191) + ->required(), + Forms\Components\TextInput::make('name') + ->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.') + ->required() + ->maxLength(60), + Forms\Components\Select::make('node_id') + ->searchable() + ->preload() + ->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.') + ->label('Linked Node') + ->relationship('node', 'name'), + ])->columns([ + 'default' => 1, + 'lg' => 2, + ]), + ]); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php b/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php index e5b72e8aa..92e4bb289 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php @@ -5,11 +5,46 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages; use App\Filament\Resources\DatabaseHostResource; use Filament\Actions; use Filament\Resources\Pages\ListRecords; +use Filament\Tables; +use Filament\Tables\Table; class ListDatabaseHosts extends ListRecords { protected static string $resource = DatabaseHostResource::class; + public function table(Table $table): Table + { + return $table + ->searchable(false) + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable(), + Tables\Columns\TextColumn::make('host') + ->searchable(), + Tables\Columns\TextColumn::make('port') + ->sortable(), + Tables\Columns\TextColumn::make('username') + ->searchable(), + Tables\Columns\TextColumn::make('max_databases') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('node.name') + ->numeric() + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/DatabaseResource.php b/app/Filament/Resources/DatabaseResource.php index b80901d7f..8f4c13069 100644 --- a/app/Filament/Resources/DatabaseResource.php +++ b/app/Filament/Resources/DatabaseResource.php @@ -4,11 +4,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\DatabaseResource\Pages; use App\Models\Database; -use Filament\Forms; -use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; -use Filament\Tables\Table; class DatabaseResource extends Resource { @@ -18,80 +14,6 @@ class DatabaseResource extends Resource protected static bool $shouldRegisterNavigation = false; - public static function form(Form $form): Form - { - return $form - ->schema([ - Forms\Components\Select::make('server_id') - ->relationship('server', 'name') - ->searchable() - ->preload() - ->required(), - Forms\Components\TextInput::make('database_host_id') - ->required() - ->numeric(), - Forms\Components\TextInput::make('database') - ->required() - ->maxLength(191), - Forms\Components\TextInput::make('remote') - ->required() - ->maxLength(191) - ->default('%'), - Forms\Components\TextInput::make('username') - ->required() - ->maxLength(191), - Forms\Components\TextInput::make('password') - ->password() - ->revealable() - ->required(), - Forms\Components\TextInput::make('max_connections') - ->numeric() - ->minValue(0) - ->default(0), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->columns([ - Tables\Columns\TextColumn::make('server.name') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('database_host_id') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('database') - ->searchable(), - Tables\Columns\TextColumn::make('username') - ->searchable(), - Tables\Columns\TextColumn::make('remote') - ->searchable(), - Tables\Columns\TextColumn::make('max_connections') - ->numeric() - ->sortable(), - Tables\Columns\TextColumn::make('created_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - Tables\Columns\TextColumn::make('updated_at') - ->dateTime() - ->sortable() - ->toggleable(isToggledHiddenByDefault: true), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - public static function getRelations(): array { return [ diff --git a/app/Filament/Resources/DatabaseResource/Pages/CreateDatabase.php b/app/Filament/Resources/DatabaseResource/Pages/CreateDatabase.php index 4f7dd312a..041a79db6 100644 --- a/app/Filament/Resources/DatabaseResource/Pages/CreateDatabase.php +++ b/app/Filament/Resources/DatabaseResource/Pages/CreateDatabase.php @@ -3,9 +3,44 @@ namespace App\Filament\Resources\DatabaseResource\Pages; use App\Filament\Resources\DatabaseResource; +use Filament\Forms\Form; use Filament\Resources\Pages\CreateRecord; +use Filament\Forms; class CreateDatabase extends CreateRecord { protected static string $resource = DatabaseResource::class; + + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Select::make('server_id') + ->relationship('server', 'name') + ->searchable() + ->preload() + ->required(), + Forms\Components\TextInput::make('database_host_id') + ->required() + ->numeric(), + Forms\Components\TextInput::make('database') + ->required() + ->maxLength(191), + Forms\Components\TextInput::make('remote') + ->required() + ->maxLength(191) + ->default('%'), + Forms\Components\TextInput::make('username') + ->required() + ->maxLength(191), + Forms\Components\TextInput::make('password') + ->password() + ->revealable() + ->required(), + Forms\Components\TextInput::make('max_connections') + ->numeric() + ->minValue(0) + ->default(0), + ]); + } } diff --git a/app/Filament/Resources/DatabaseResource/Pages/EditDatabase.php b/app/Filament/Resources/DatabaseResource/Pages/EditDatabase.php index 5103ef894..2ff5af76b 100644 --- a/app/Filament/Resources/DatabaseResource/Pages/EditDatabase.php +++ b/app/Filament/Resources/DatabaseResource/Pages/EditDatabase.php @@ -4,12 +4,47 @@ namespace App\Filament\Resources\DatabaseResource\Pages; use App\Filament\Resources\DatabaseResource; use Filament\Actions; +use Filament\Forms\Form; use Filament\Resources\Pages\EditRecord; +use Filament\Forms; class EditDatabase extends EditRecord { protected static string $resource = DatabaseResource::class; + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Select::make('server_id') + ->relationship('server', 'name') + ->searchable() + ->preload() + ->required(), + Forms\Components\TextInput::make('database_host_id') + ->required() + ->numeric(), + Forms\Components\TextInput::make('database') + ->required() + ->maxLength(191), + Forms\Components\TextInput::make('remote') + ->required() + ->maxLength(191) + ->default('%'), + Forms\Components\TextInput::make('username') + ->required() + ->maxLength(191), + Forms\Components\TextInput::make('password') + ->password() + ->revealable() + ->required(), + Forms\Components\TextInput::make('max_connections') + ->numeric() + ->minValue(0) + ->default(0), + ]); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Resources/DatabaseResource/Pages/ListDatabases.php index 687637a7e..fa10aa015 100644 --- a/app/Filament/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Resources/DatabaseResource/Pages/ListDatabases.php @@ -5,11 +5,54 @@ namespace App\Filament\Resources\DatabaseResource\Pages; use App\Filament\Resources\DatabaseResource; use Filament\Actions; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Table; +use Filament\Tables; class ListDatabases extends ListRecords { protected static string $resource = DatabaseResource::class; + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('server.name') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('database_host_id') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('database') + ->searchable(), + Tables\Columns\TextColumn::make('username') + ->searchable(), + Tables\Columns\TextColumn::make('remote') + ->searchable(), + Tables\Columns\TextColumn::make('max_connections') + ->numeric() + ->sortable(), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/EggResource.php b/app/Filament/Resources/EggResource.php index c2e7875e5..9bee841e8 100644 --- a/app/Filament/Resources/EggResource.php +++ b/app/Filament/Resources/EggResource.php @@ -3,13 +3,8 @@ namespace App\Filament\Resources; use App\Filament\Resources\EggResource\Pages; -use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; use App\Models\Egg; -use Filament\Forms; -use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; -use Filament\Tables\Table; class EggResource extends Resource { @@ -21,213 +16,6 @@ class EggResource extends Resource protected static ?string $recordRouteKeyName = 'id'; - public static function form(Form $form): Form - { - return $form - ->schema([ - Forms\Components\Tabs::make()->tabs([ - Forms\Components\Tabs\Tab::make('Configuration') - ->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4]) - ->schema([ - Forms\Components\TextInput::make('name') - ->required() - ->maxLength(191) - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) - ->helperText('A simple, human-readable name to use as an identifier for this Egg.'), - Forms\Components\TextInput::make('uuid') - ->disabled() - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) - ->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'), - Forms\Components\Textarea::make('description') - ->rows(3) - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) - ->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'), - Forms\Components\TextInput::make('author') - ->required() - ->maxLength(191) - ->disabled() - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) - ->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'), - Forms\Components\Textarea::make('startup') - ->rows(2) - ->columnSpanFull() - ->required() - ->helperText('The default startup command that should be used for new servers using this Egg.'), - Forms\Components\TagsInput::make('file_denylist') - ->placeholder('denied-file.txt') - ->helperText('A list of files that the end user is not allowed to edit.') - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), - Forms\Components\TagsInput::make('features') - ->placeholder('Add Feature') - ->helperText('') - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), - Forms\Components\Toggle::make('force_outgoing_ip') - ->helperText("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP. - Required for certain games to work properly when the Node has multiple public IP addresses. - Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."), - Forms\Components\Toggle::make('script_is_privileged') - ->helperText('The docker images available to servers using this egg.'), - Forms\Components\TextInput::make('update_url') - ->disabled() - ->helperText('Not implemented.') - ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), - Forms\Components\KeyValue::make('docker_images') - ->columnSpanFull() - ->required() - ->addActionLabel('Add Image') - ->keyLabel('Name') - ->valueLabel('Image URI') - ->helperText('The docker images available to servers using this egg.'), - ]), - - Forms\Components\Tabs\Tab::make('Process Management') - ->columns() - ->schema([ - Forms\Components\Select::make('config_from') - ->label('Copy Settings From') - ->placeholder('None') - ->relationship('configFrom', 'name', ignoreRecord: true) - ->helperText('If you would like to default to settings from another Egg select it from the menu above.'), - Forms\Components\TextInput::make('config_stop') - ->maxLength(191) - ->label('Stop Command') - ->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'), - Forms\Components\Textarea::make('config_startup')->rows(10)->json() - ->label('Start Configuration') - ->helperText('List of values the daemon should be looking for when booting a server to determine completion.'), - Forms\Components\Textarea::make('config_files')->rows(10)->json() - ->label('Configuration Files') - ->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'), - Forms\Components\Textarea::make('config_logs')->rows(10)->json() - ->label('Log Configuration') - ->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'), - ]), - Forms\Components\Tabs\Tab::make('Egg Variables') - ->columnSpanFull() - ->columns(2) - ->schema([ - Forms\Components\Repeater::make('variables') - ->grid() - ->relationship('variables') - ->name('name') - ->columns(2) - ->reorderable() - ->collapsible() - ->collapsed() - ->orderColumn() - ->columnSpan(2) - ->itemLabel(fn (array $state) => $state['name']) - ->mutateRelationshipDataBeforeCreateUsing(function (array $data): array { - $data['default_value'] ??= ''; - $data['description'] ??= ''; - $data['rules'] ??= ''; - - return $data; - }) - ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { - $data['default_value'] ??= ''; - $data['description'] ??= ''; - $data['rules'] ??= ''; - - return $data; - }) - ->schema([ - Forms\Components\TextInput::make('name') - ->live() - ->debounce(750) - ->maxLength(191) - ->columnSpanFull() - ->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()) - ) - ->required(), - Forms\Components\Textarea::make('description')->columnSpanFull(), - Forms\Components\TextInput::make('env_variable') - ->label('Environment Variable') - ->maxLength(191) - ->hint(fn ($state) => "{{{$state}}}") - ->required(), - Forms\Components\TextInput::make('default_value')->maxLength(191), - Forms\Components\Textarea::make('rules')->rows(3)->columnSpanFull(), - ]), - ]), - Forms\Components\Tabs\Tab::make('Install Script') - ->columns(3) - ->schema([ - - Forms\Components\Select::make('copy_script_from') - ->placeholder('None') - ->relationship('scriptFrom', 'name', ignoreRecord: true), - - Forms\Components\TextInput::make('script_container') - ->required() - ->maxLength(191) - ->default('alpine:3.4'), - - Forms\Components\TextInput::make('script_entry') - ->required() - ->maxLength(191) - ->default('ash'), - - MonacoEditor::make('script_install') - ->columnSpanFull() - ->fontSize('16px') - ->language('shell') - ->view('filament.plugins.monaco-editor'), - ]), - - ])->columnSpanFull()->persistTabInQueryString(), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->searchable(false) - ->defaultPaginationPageOption(25) - ->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0) - ->columns([ - Tables\Columns\TextColumn::make('id') - ->label('Id') - ->hidden() - ->searchable(), - Tables\Columns\TextColumn::make('name') - ->icon('tabler-egg') - ->description(fn ($record): ?string => $record->description) - ->wrap() - ->searchable(), - Tables\Columns\TextColumn::make('author') - ->hidden() - ->searchable(), - Tables\Columns\TextColumn::make('servers_count') - ->counts('servers') - ->icon('tabler-server') - ->label('Servers'), - Tables\Columns\TextColumn::make('script_container') - ->searchable() - ->hidden(), - Tables\Columns\TextColumn::make('copyFrom.name') - ->hidden() - ->sortable(), - Tables\Columns\TextColumn::make('script_entry') - ->hidden() - ->searchable(), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->headerActions([ - // - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - public static function getRelations(): array { return [ diff --git a/app/Filament/Resources/EggResource/Pages/CreateEgg.php b/app/Filament/Resources/EggResource/Pages/CreateEgg.php index 1cdd576b8..cdb18c6fe 100644 --- a/app/Filament/Resources/EggResource/Pages/CreateEgg.php +++ b/app/Filament/Resources/EggResource/Pages/CreateEgg.php @@ -4,8 +4,168 @@ namespace App\Filament\Resources\EggResource\Pages; use App\Filament\Resources\EggResource; use Filament\Resources\Pages\CreateRecord; +use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; +use Filament\Forms; +use Filament\Forms\Form; class CreateEgg extends CreateRecord { protected static string $resource = EggResource::class; + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Tabs::make()->tabs([ + Forms\Components\Tabs\Tab::make('Configuration') + ->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4]) + ->schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(191) + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->helperText('A simple, human-readable name to use as an identifier for this Egg.'), + Forms\Components\TextInput::make('uuid') + ->disabled() + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'), + Forms\Components\Textarea::make('description') + ->rows(3) + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'), + Forms\Components\TextInput::make('author') + ->required() + ->maxLength(191) + ->disabled() + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'), + Forms\Components\Textarea::make('startup') + ->rows(2) + ->columnSpanFull() + ->required() + ->helperText('The default startup command that should be used for new servers using this Egg.'), + Forms\Components\TagsInput::make('file_denylist') + ->placeholder('denied-file.txt') + ->helperText('A list of files that the end user is not allowed to edit.') + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), + Forms\Components\TagsInput::make('features') + ->placeholder('Add Feature') + ->helperText('') + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), + Forms\Components\Toggle::make('force_outgoing_ip') + ->helperText("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP. + Required for certain games to work properly when the Node has multiple public IP addresses. + Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."), + Forms\Components\Toggle::make('script_is_privileged') + ->helperText('The docker images available to servers using this egg.'), + Forms\Components\TextInput::make('update_url') + ->disabled() + ->helperText('Not implemented.') + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), + Forms\Components\KeyValue::make('docker_images') + ->columnSpanFull() + ->required() + ->addActionLabel('Add Image') + ->keyLabel('Name') + ->valueLabel('Image URI') + ->helperText('The docker images available to servers using this egg.'), + ]), + + Forms\Components\Tabs\Tab::make('Process Management') + ->columns() + ->schema([ + Forms\Components\Select::make('config_from') + ->label('Copy Settings From') + ->placeholder('None') + ->relationship('configFrom', 'name', ignoreRecord: true) + ->helperText('If you would like to default to settings from another Egg select it from the menu above.'), + Forms\Components\TextInput::make('config_stop') + ->maxLength(191) + ->label('Stop Command') + ->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'), + Forms\Components\Textarea::make('config_startup')->rows(10)->json() + ->label('Start Configuration') + ->helperText('List of values the daemon should be looking for when booting a server to determine completion.'), + Forms\Components\Textarea::make('config_files')->rows(10)->json() + ->label('Configuration Files') + ->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'), + Forms\Components\Textarea::make('config_logs')->rows(10)->json() + ->label('Log Configuration') + ->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'), + ]), + Forms\Components\Tabs\Tab::make('Egg Variables') + ->columnSpanFull() + ->columns(2) + ->schema([ + Forms\Components\Repeater::make('variables') + ->grid() + ->relationship('variables') + ->name('name') + ->columns(2) + ->reorderable() + ->collapsible() + ->collapsed() + ->orderColumn() + ->columnSpan(2) + ->itemLabel(fn (array $state) => $state['name']) + ->mutateRelationshipDataBeforeCreateUsing(function (array $data): array { + $data['default_value'] ??= ''; + $data['description'] ??= ''; + $data['rules'] ??= ''; + + return $data; + }) + ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { + $data['default_value'] ??= ''; + $data['description'] ??= ''; + $data['rules'] ??= ''; + + return $data; + }) + ->schema([ + Forms\Components\TextInput::make('name') + ->live() + ->debounce(750) + ->maxLength(191) + ->columnSpanFull() + ->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()) + ) + ->required(), + Forms\Components\Textarea::make('description')->columnSpanFull(), + Forms\Components\TextInput::make('env_variable') + ->label('Environment Variable') + ->maxLength(191) + ->hint(fn ($state) => "{{{$state}}}") + ->required(), + Forms\Components\TextInput::make('default_value')->maxLength(191), + Forms\Components\Textarea::make('rules')->rows(3)->columnSpanFull(), + ]), + ]), + Forms\Components\Tabs\Tab::make('Install Script') + ->columns(3) + ->schema([ + + Forms\Components\Select::make('copy_script_from') + ->placeholder('None') + ->relationship('scriptFrom', 'name', ignoreRecord: true), + + Forms\Components\TextInput::make('script_container') + ->required() + ->maxLength(191) + ->default('alpine:3.4'), + + Forms\Components\TextInput::make('script_entry') + ->required() + ->maxLength(191) + ->default('ash'), + + MonacoEditor::make('script_install') + ->columnSpanFull() + ->fontSize('16px') + ->language('shell') + ->view('filament.plugins.monaco-editor'), + ]), + + ])->columnSpanFull()->persistTabInQueryString(), + ]); + } } diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index 2f44bd343..2753e91ae 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -5,11 +5,172 @@ namespace App\Filament\Resources\EggResource\Pages; use App\Filament\Resources\EggResource; use Filament\Actions; use Filament\Resources\Pages\EditRecord; +use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; +use Filament\Forms; +use Filament\Forms\Form; class EditEgg extends EditRecord { protected static string $resource = EggResource::class; + public function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Tabs::make()->tabs([ + Forms\Components\Tabs\Tab::make('Configuration') + ->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4]) + ->schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(191) + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->helperText('A simple, human-readable name to use as an identifier for this Egg.'), + Forms\Components\TextInput::make('uuid') + ->disabled() + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'), + Forms\Components\Textarea::make('description') + ->rows(3) + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'), + Forms\Components\TextInput::make('author') + ->required() + ->maxLength(191) + ->disabled() + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) + ->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'), + Forms\Components\Textarea::make('startup') + ->rows(2) + ->columnSpanFull() + ->required() + ->helperText('The default startup command that should be used for new servers using this Egg.'), + Forms\Components\TagsInput::make('file_denylist') + ->placeholder('denied-file.txt') + ->helperText('A list of files that the end user is not allowed to edit.') + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), + Forms\Components\TagsInput::make('features') + ->placeholder('Add Feature') + ->helperText('') + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), + Forms\Components\Toggle::make('force_outgoing_ip') + ->helperText("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP. + Required for certain games to work properly when the Node has multiple public IP addresses. + Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."), + Forms\Components\Toggle::make('script_is_privileged') + ->helperText('The docker images available to servers using this egg.'), + Forms\Components\TextInput::make('update_url') + ->disabled() + ->helperText('Not implemented.') + ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), + Forms\Components\KeyValue::make('docker_images') + ->columnSpanFull() + ->required() + ->addActionLabel('Add Image') + ->keyLabel('Name') + ->valueLabel('Image URI') + ->helperText('The docker images available to servers using this egg.'), + ]), + + Forms\Components\Tabs\Tab::make('Process Management') + ->columns() + ->schema([ + Forms\Components\Select::make('config_from') + ->label('Copy Settings From') + ->placeholder('None') + ->relationship('configFrom', 'name', ignoreRecord: true) + ->helperText('If you would like to default to settings from another Egg select it from the menu above.'), + Forms\Components\TextInput::make('config_stop') + ->maxLength(191) + ->label('Stop Command') + ->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'), + Forms\Components\Textarea::make('config_startup')->rows(10)->json() + ->label('Start Configuration') + ->helperText('List of values the daemon should be looking for when booting a server to determine completion.'), + Forms\Components\Textarea::make('config_files')->rows(10)->json() + ->label('Configuration Files') + ->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'), + Forms\Components\Textarea::make('config_logs')->rows(10)->json() + ->label('Log Configuration') + ->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'), + ]), + Forms\Components\Tabs\Tab::make('Egg Variables') + ->columnSpanFull() + ->columns(2) + ->schema([ + Forms\Components\Repeater::make('variables') + ->grid() + ->relationship('variables') + ->name('name') + ->columns(2) + ->reorderable() + ->collapsible() + ->collapsed() + ->orderColumn() + ->columnSpan(2) + ->itemLabel(fn (array $state) => $state['name']) + ->mutateRelationshipDataBeforeCreateUsing(function (array $data): array { + $data['default_value'] ??= ''; + $data['description'] ??= ''; + $data['rules'] ??= ''; + + return $data; + }) + ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { + $data['default_value'] ??= ''; + $data['description'] ??= ''; + $data['rules'] ??= ''; + + return $data; + }) + ->schema([ + Forms\Components\TextInput::make('name') + ->live() + ->debounce(750) + ->maxLength(191) + ->columnSpanFull() + ->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()) + ) + ->required(), + Forms\Components\Textarea::make('description')->columnSpanFull(), + Forms\Components\TextInput::make('env_variable') + ->label('Environment Variable') + ->maxLength(191) + ->hint(fn ($state) => "{{{$state}}}") + ->required(), + Forms\Components\TextInput::make('default_value')->maxLength(191), + Forms\Components\Textarea::make('rules')->rows(3)->columnSpanFull(), + ]), + ]), + Forms\Components\Tabs\Tab::make('Install Script') + ->columns(3) + ->schema([ + + Forms\Components\Select::make('copy_script_from') + ->placeholder('None') + ->relationship('scriptFrom', 'name', ignoreRecord: true), + + Forms\Components\TextInput::make('script_container') + ->required() + ->maxLength(191) + ->default('alpine:3.4'), + + Forms\Components\TextInput::make('script_entry') + ->required() + ->maxLength(191) + ->default('ash'), + + MonacoEditor::make('script_install') + ->columnSpanFull() + ->fontSize('16px') + ->language('shell') + ->view('filament.plugins.monaco-editor'), + ]), + + ])->columnSpanFull()->persistTabInQueryString(), + ]); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/EggResource/Pages/ListEggs.php b/app/Filament/Resources/EggResource/Pages/ListEggs.php index 00be09020..275bc4206 100644 --- a/app/Filament/Resources/EggResource/Pages/ListEggs.php +++ b/app/Filament/Resources/EggResource/Pages/ListEggs.php @@ -3,18 +3,69 @@ namespace App\Filament\Resources\EggResource\Pages; use App\Filament\Resources\EggResource; +use App\Models\Egg; use App\Services\Eggs\Sharing\EggImporterService; use Exception; use Filament\Actions; use Filament\Forms; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Table; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; +use Filament\Tables; class ListEggs extends ListRecords { protected static string $resource = EggResource::class; + public function table(Table $table): Table + { + return $table + ->searchable(false) + ->defaultPaginationPageOption(25) + ->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0) + ->columns([ + Tables\Columns\TextColumn::make('id') + ->label('Id') + ->hidden() + ->searchable(), + Tables\Columns\TextColumn::make('name') + ->icon('tabler-egg') + ->description(fn ($record): ?string => $record->description) + ->wrap() + ->searchable(), + Tables\Columns\TextColumn::make('author') + ->hidden() + ->searchable(), + Tables\Columns\TextColumn::make('servers_count') + ->counts('servers') + ->icon('tabler-server') + ->label('Servers'), + Tables\Columns\TextColumn::make('script_container') + ->searchable() + ->hidden(), + Tables\Columns\TextColumn::make('copyFrom.name') + ->hidden() + ->sortable(), + Tables\Columns\TextColumn::make('script_entry') + ->hidden() + ->searchable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->headerActions([ + // + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/MountResource.php b/app/Filament/Resources/MountResource.php index b3c9f3273..1c9a0c45a 100644 --- a/app/Filament/Resources/MountResource.php +++ b/app/Filament/Resources/MountResource.php @@ -4,14 +4,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\MountResource\Pages; use App\Models\Mount; -use Filament\Forms; -use Filament\Forms\Components\Group; -use Filament\Forms\Components\Section; -use Filament\Forms\Components\Select; -use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; -use Filament\Tables\Table; class MountResource extends Resource { @@ -19,120 +12,6 @@ class MountResource extends Resource protected static ?string $navigationIcon = 'tabler-layers-linked'; - public static function form(Form $form): Form - { - return $form - ->schema([ - Section::make()->schema([ - Forms\Components\TextInput::make('name') - ->required() - ->helperText('Unique name used to separate this mount from another.') - ->maxLength(64), - Forms\Components\ToggleButtons::make('read_only') - ->label('Read only?') - ->helperText('Is the mount read only inside the container?') - ->options([ - false => 'Writeable', - true => 'Read only', - ]) - ->icons([ - false => 'tabler-writing', - true => 'tabler-writing-off', - ]) - ->colors([ - false => 'warning', - true => 'success', - ]) - ->inline() - ->default(false) - ->required(), - Forms\Components\TextInput::make('source') - ->required() - ->helperText('File path on the host system to mount to a container.') - ->maxLength(191), - Forms\Components\TextInput::make('target') - ->required() - ->helperText('Where the mount will be accessible inside a container.') - ->maxLength(191), - Forms\Components\ToggleButtons::make('user_mountable') - ->hidden() - ->label('User mountable?') - ->options([ - false => 'No', - true => 'Yes', - ]) - ->icons([ - false => 'tabler-user-cancel', - true => 'tabler-user-bolt', - ]) - ->colors([ - false => 'success', - true => 'warning', - ]) - ->default(false) - ->inline() - ->required(), - Forms\Components\Textarea::make('description') - ->helperText('A longer description for this mount.') - ->columnSpanFull(), - ])->columnSpan(1)->columns([ - 'default' => 1, - 'lg' => 2, - ]), - Group::make()->schema([ - Section::make()->schema([ - Select::make('eggs')->multiple() - ->relationship('eggs', 'name') - ->preload(), - Select::make('nodes')->multiple() - ->relationship('nodes', 'name') - ->searchable(['name', 'fqdn']) - ->preload(), - ]), - ])->columns([ - 'default' => 1, - 'lg' => 2, - ]), - ])->columns([ - 'default' => 1, - 'lg' => 2, - ]); - } - - public static function table(Table $table): Table - { - return $table - ->searchable(false) - ->columns([ - Tables\Columns\TextColumn::make('name') - ->searchable(), - Tables\Columns\TextColumn::make('source') - ->searchable(), - Tables\Columns\TextColumn::make('target') - ->searchable(), - Tables\Columns\IconColumn::make('read_only') - ->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled') - ->color(fn (bool $state) => $state ? 'success' : 'danger') - ->sortable(), - Tables\Columns\IconColumn::make('user_mountable') - ->hidden() - ->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled') - ->color(fn (bool $state) => $state ? 'success' : 'danger') - ->sortable(), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - public static function getRelations(): array { return [ diff --git a/app/Filament/Resources/MountResource/Pages/CreateMount.php b/app/Filament/Resources/MountResource/Pages/CreateMount.php index 54ac86c4d..46eca13c0 100644 --- a/app/Filament/Resources/MountResource/Pages/CreateMount.php +++ b/app/Filament/Resources/MountResource/Pages/CreateMount.php @@ -3,9 +3,93 @@ namespace App\Filament\Resources\MountResource\Pages; use App\Filament\Resources\MountResource; +use Filament\Forms\Components\Group; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; +use Filament\Forms\Form; use Filament\Resources\Pages\CreateRecord; +use Filament\Forms; class CreateMount extends CreateRecord { protected static string $resource = MountResource::class; + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make()->schema([ + Forms\Components\TextInput::make('name') + ->required() + ->helperText('Unique name used to separate this mount from another.') + ->maxLength(64), + Forms\Components\ToggleButtons::make('read_only') + ->label('Read only?') + ->helperText('Is the mount read only inside the container?') + ->options([ + false => 'Writeable', + true => 'Read only', + ]) + ->icons([ + false => 'tabler-writing', + true => 'tabler-writing-off', + ]) + ->colors([ + false => 'warning', + true => 'success', + ]) + ->inline() + ->default(false) + ->required(), + Forms\Components\TextInput::make('source') + ->required() + ->helperText('File path on the host system to mount to a container.') + ->maxLength(191), + Forms\Components\TextInput::make('target') + ->required() + ->helperText('Where the mount will be accessible inside a container.') + ->maxLength(191), + Forms\Components\ToggleButtons::make('user_mountable') + ->hidden() + ->label('User mountable?') + ->options([ + false => 'No', + true => 'Yes', + ]) + ->icons([ + false => 'tabler-user-cancel', + true => 'tabler-user-bolt', + ]) + ->colors([ + false => 'success', + true => 'warning', + ]) + ->default(false) + ->inline() + ->required(), + Forms\Components\Textarea::make('description') + ->helperText('A longer description for this mount.') + ->columnSpanFull(), + ])->columnSpan(1)->columns([ + 'default' => 1, + 'lg' => 2, + ]), + Group::make()->schema([ + Section::make()->schema([ + Select::make('eggs')->multiple() + ->relationship('eggs', 'name') + ->preload(), + Select::make('nodes')->multiple() + ->relationship('nodes', 'name') + ->searchable(['name', 'fqdn']) + ->preload(), + ]), + ])->columns([ + 'default' => 1, + 'lg' => 2, + ]), + ])->columns([ + 'default' => 1, + 'lg' => 2, + ]); + } } diff --git a/app/Filament/Resources/MountResource/Pages/EditMount.php b/app/Filament/Resources/MountResource/Pages/EditMount.php index f01498c7b..fefccb89c 100644 --- a/app/Filament/Resources/MountResource/Pages/EditMount.php +++ b/app/Filament/Resources/MountResource/Pages/EditMount.php @@ -5,11 +5,95 @@ namespace App\Filament\Resources\MountResource\Pages; use App\Filament\Resources\MountResource; use Filament\Actions; use Filament\Resources\Pages\EditRecord; +use Filament\Forms; +use Filament\Forms\Components\Group; +use Filament\Forms\Components\Section; +use Filament\Forms\Components\Select; +use Filament\Forms\Form; class EditMount extends EditRecord { protected static string $resource = MountResource::class; + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make()->schema([ + Forms\Components\TextInput::make('name') + ->required() + ->helperText('Unique name used to separate this mount from another.') + ->maxLength(64), + Forms\Components\ToggleButtons::make('read_only') + ->label('Read only?') + ->helperText('Is the mount read only inside the container?') + ->options([ + false => 'Writeable', + true => 'Read only', + ]) + ->icons([ + false => 'tabler-writing', + true => 'tabler-writing-off', + ]) + ->colors([ + false => 'warning', + true => 'success', + ]) + ->inline() + ->default(false) + ->required(), + Forms\Components\TextInput::make('source') + ->required() + ->helperText('File path on the host system to mount to a container.') + ->maxLength(191), + Forms\Components\TextInput::make('target') + ->required() + ->helperText('Where the mount will be accessible inside a container.') + ->maxLength(191), + Forms\Components\ToggleButtons::make('user_mountable') + ->hidden() + ->label('User mountable?') + ->options([ + false => 'No', + true => 'Yes', + ]) + ->icons([ + false => 'tabler-user-cancel', + true => 'tabler-user-bolt', + ]) + ->colors([ + false => 'success', + true => 'warning', + ]) + ->default(false) + ->inline() + ->required(), + Forms\Components\Textarea::make('description') + ->helperText('A longer description for this mount.') + ->columnSpanFull(), + ])->columnSpan(1)->columns([ + 'default' => 1, + 'lg' => 2, + ]), + Group::make()->schema([ + Section::make()->schema([ + Select::make('eggs')->multiple() + ->relationship('eggs', 'name') + ->preload(), + Select::make('nodes')->multiple() + ->relationship('nodes', 'name') + ->searchable(['name', 'fqdn']) + ->preload(), + ]), + ])->columns([ + 'default' => 1, + 'lg' => 2, + ]), + ])->columns([ + 'default' => 1, + 'lg' => 2, + ]); + } protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/MountResource/Pages/ListMounts.php b/app/Filament/Resources/MountResource/Pages/ListMounts.php index d772317ed..aeb68d1a7 100644 --- a/app/Filament/Resources/MountResource/Pages/ListMounts.php +++ b/app/Filament/Resources/MountResource/Pages/ListMounts.php @@ -5,11 +5,45 @@ namespace App\Filament\Resources\MountResource\Pages; use App\Filament\Resources\MountResource; use Filament\Actions; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Table; +use Filament\Tables; class ListMounts extends ListRecords { protected static string $resource = MountResource::class; - + public function table(Table $table): Table + { + return $table + ->searchable(false) + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable(), + Tables\Columns\TextColumn::make('source') + ->searchable(), + Tables\Columns\TextColumn::make('target') + ->searchable(), + Tables\Columns\IconColumn::make('read_only') + ->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled') + ->color(fn (bool $state) => $state ? 'success' : 'danger') + ->sortable(), + Tables\Columns\IconColumn::make('user_mountable') + ->hidden() + ->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled') + ->color(fn (bool $state) => $state ? 'success' : 'danger') + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/NodeResource.php b/app/Filament/Resources/NodeResource.php index 4c9f30a56..a16e47d36 100644 --- a/app/Filament/Resources/NodeResource.php +++ b/app/Filament/Resources/NodeResource.php @@ -5,11 +5,7 @@ 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; class NodeResource extends Resource { @@ -19,133 +15,6 @@ class NodeResource extends Resource protected static ?string $recordTitleAttribute = 'name'; - public static function form(Form $form): Form - { - return $form - ->schema([ - Forms\Components\Toggle::make('behind_proxy') - ->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() - ->numeric(), - Forms\Components\TextInput::make('memory_overallocate') - ->required() - ->numeric() - ->default(0), - Forms\Components\TextInput::make('disk') - ->required() - ->numeric(), - Forms\Components\TextInput::make('disk_overallocate') - ->required() - ->numeric() - ->default(0), - Forms\Components\TextInput::make('upload_size') - ->required() - ->integer() - ->default(100), - Forms\Components\TextInput::make('daemon_listen') - ->required() - ->integer() - ->label('Daemon Port') - ->default(8080), - Forms\Components\TextInput::make('daemon_sftp') - ->required() - ->integer() - ->label('Daemon SFTP Port') - ->default(2022), - Forms\Components\TextInput::make('daemon_base') - ->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 => 'tabler-eye-check', - false => 'tabler-eye-cancel', - ]), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->searchable(false) - ->columns([ - Tables\Columns\TextColumn::make('uuid') - ->label('UUID') - ->searchable() - ->hidden(), - Tables\Columns\IconColumn::make('health') - ->alignCenter() - ->state(fn (Node $node) => $node) - ->view('livewire.columns.version-column'), - Tables\Columns\TextColumn::make('name') - ->icon('tabler-server-2') - ->sortable() - ->searchable(), - Tables\Columns\TextColumn::make('fqdn') - ->visibleFrom('md') - ->label('Address') - ->icon('tabler-network') - ->sortable() - ->searchable(), - Tables\Columns\TextColumn::make('memory') - ->visibleFrom('sm') - ->icon('tabler-device-desktop-analytics') - ->numeric() - ->suffix(' GB') - ->formatStateUsing(fn ($state) => number_format($state / 1000, 2)) - ->sortable(), - Tables\Columns\TextColumn::make('disk') - ->visibleFrom('sm') - ->icon('tabler-file') - ->numeric() - ->suffix(' GB') - ->formatStateUsing(fn ($state) => number_format($state / 1000, 2)) - ->sortable(), - Tables\Columns\IconColumn::make('scheme') - ->visibleFrom('xl') - ->label('SSL') - ->trueIcon('tabler-lock') - ->falseIcon('tabler-lock-open-off') - ->state(fn (Node $node) => $node->scheme === 'https'), - Tables\Columns\IconColumn::make('public') - ->visibleFrom('lg') - ->trueIcon('tabler-eye-check') - ->falseIcon('tabler-eye-cancel'), - Tables\Columns\TextColumn::make('servers_count') - ->visibleFrom('sm') - ->counts('servers') - ->label('Servers') - ->sortable() - ->icon('tabler-brand-docker'), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - public static function getRelations(): array { return [ diff --git a/app/Filament/Resources/NodeResource/Pages/ListNodes.php b/app/Filament/Resources/NodeResource/Pages/ListNodes.php index d7fd0307c..dd89dd1b8 100644 --- a/app/Filament/Resources/NodeResource/Pages/ListNodes.php +++ b/app/Filament/Resources/NodeResource/Pages/ListNodes.php @@ -3,13 +3,83 @@ namespace App\Filament\Resources\NodeResource\Pages; use App\Filament\Resources\NodeResource; +use App\Models\Node; use Filament\Actions; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Table; +use Filament\Tables; class ListNodes extends ListRecords { protected static string $resource = NodeResource::class; + public function table(Table $table): Table + { + return $table + ->searchable(false) + ->columns([ + Tables\Columns\TextColumn::make('uuid') + ->label('UUID') + ->searchable() + ->hidden(), + Tables\Columns\IconColumn::make('health') + ->alignCenter() + ->state(fn (Node $node) => $node) + ->view('livewire.columns.version-column'), + Tables\Columns\TextColumn::make('name') + ->icon('tabler-server-2') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('fqdn') + ->visibleFrom('md') + ->label('Address') + ->icon('tabler-network') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('memory') + ->visibleFrom('sm') + ->icon('tabler-device-desktop-analytics') + ->numeric() + ->suffix(' GB') + ->formatStateUsing(fn ($state) => number_format($state / 1000, 2)) + ->sortable(), + Tables\Columns\TextColumn::make('disk') + ->visibleFrom('sm') + ->icon('tabler-file') + ->numeric() + ->suffix(' GB') + ->formatStateUsing(fn ($state) => number_format($state / 1000, 2)) + ->sortable(), + Tables\Columns\IconColumn::make('scheme') + ->visibleFrom('xl') + ->label('SSL') + ->trueIcon('tabler-lock') + ->falseIcon('tabler-lock-open-off') + ->state(fn (Node $node) => $node->scheme === 'https'), + Tables\Columns\IconColumn::make('public') + ->visibleFrom('lg') + ->trueIcon('tabler-eye-check') + ->falseIcon('tabler-eye-cancel'), + Tables\Columns\TextColumn::make('servers_count') + ->visibleFrom('sm') + ->counts('servers') + ->label('Servers') + ->sortable() + ->icon('tabler-brand-docker'), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/ServerResource.php b/app/Filament/Resources/ServerResource.php index aac79bcd7..227950431 100644 --- a/app/Filament/Resources/ServerResource.php +++ b/app/Filament/Resources/ServerResource.php @@ -2,25 +2,9 @@ namespace App\Filament\Resources; -use App\Enums\ContainerStatus; -use App\Enums\ServerState; use App\Filament\Resources\ServerResource\Pages; -use App\Models\Allocation; -use App\Models\Egg; -use App\Models\Node; use App\Models\Server; -use App\Models\ServerVariable; -use App\Repositories\Daemon\DaemonServerRepository; -use App\Services\Allocations\AssignmentService; -use Closure; -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\Support\Arr; -use Illuminate\Support\Facades\Validator; class ServerResource extends Resource { @@ -30,673 +14,6 @@ class ServerResource extends Resource protected static ?string $recordTitleAttribute = 'name'; - public static function form(Form $form): Form - { - return $form - ->columns([ - 'default' => 2, - 'sm' => 2, - 'md' => 4, - 'lg' => 6, - ]) - ->schema([ - Forms\Components\ToggleButtons::make('docker') - ->label('Container Status') - ->hiddenOn('create') - ->inlineLabel() - ->formatStateUsing(function ($state, Server $server) { - if ($server->node_id === null) { - return 'unknown'; - } - - /** @var DaemonServerRepository $service */ - $service = resolve(DaemonServerRepository::class); - $details = $service->setServer($server)->getDetails(); - - return $details['state'] ?? 'unknown'; - }) - ->options(fn ($state) => collect(ContainerStatus::cases())->filter(fn ($containerStatus) => $containerStatus->value === $state)->mapWithKeys( - fn (ContainerStatus $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()] - )) - ->colors(collect(ContainerStatus::cases())->mapWithKeys( - fn (ContainerStatus $status) => [$status->value => $status->color()] - )) - ->icons(collect(ContainerStatus::cases())->mapWithKeys( - fn (ContainerStatus $status) => [$status->value => $status->icon()] - )) - ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 3, - ]) - ->inline(), - - Forms\Components\ToggleButtons::make('status') - ->label('Server State') - ->helperText('') - ->hiddenOn('create') - ->inlineLabel() - ->formatStateUsing(fn ($state) => $state ?? ServerState::Normal) - ->options(fn ($state) => collect(ServerState::cases())->filter(fn ($serverState) => $serverState->value === $state)->mapWithKeys( - fn (ServerState $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()] - )) - ->colors(collect(ServerState::cases())->mapWithKeys( - fn (ServerState $state) => [$state->value => $state->color()] - )) - ->icons(collect(ServerState::cases())->mapWithKeys( - fn (ServerState $state) => [$state->value => $state->icon()] - )) - ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 3, - ]) - ->inline(), - - Forms\Components\TextInput::make('external_id') - ->maxLength(191) - ->hidden(), - - Forms\Components\TextInput::make('name') - ->prefixIcon('tabler-server') - ->label('Display Name') - ->suffixAction(Forms\Components\Actions\Action::make('random') - ->icon('tabler-dice-' . random_int(1, 6)) - ->color('primary') - ->action(function (Forms\Set $set, Forms\Get $get) { - $egg = Egg::find($get('egg_id')); - $prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : ''; - - $set('name', $prefix . fake()->domainWord); - })) - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 2, - 'lg' => 3, - ]) - ->required() - ->maxLength(191), - - Forms\Components\Select::make('owner_id') - ->prefixIcon('tabler-user') - ->default(auth()->user()->id) - ->label('Owner') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 2, - 'lg' => 3, - ]) - ->relationship('user', 'username') - ->searchable() - ->preload() - ->required(), - - Forms\Components\Select::make('node_id') - ->disabledOn('edit') - ->prefixIcon('tabler-server-2') - ->default(fn () => Node::query()->latest()->first()?->id) - ->columnSpan(2) - ->live() - ->relationship('node', 'name') - ->searchable() - ->preload() - ->afterStateUpdated(fn (Forms\Set $set) => $set('allocation_id', null)) - ->required(), - - Forms\Components\Select::make('allocation_id') - ->preload() - ->live() - ->prefixIcon('tabler-network') - ->label('Primary Allocation') - ->columnSpan(2) - ->disabled(fn (Forms\Get $get) => $get('node_id') === null) - ->searchable(['ip', 'port', 'ip_alias']) - ->afterStateUpdated(function (Forms\Set $set) { - $set('allocation_additional', null); - $set('allocation_additional.needstobeastringhere.extra_allocations', null); - }) - ->getOptionLabelFromRecordUsing( - fn (Allocation $allocation) => "$allocation->ip:$allocation->port" . - ($allocation->ip_alias ? " ($allocation->ip_alias)" : '') - ) - ->placeholder(function (Forms\Get $get) { - $node = Node::find($get('node_id')); - - if ($node?->allocations) { - return 'Select an Allocation'; - } - - return 'Create a New Allocation'; - }) - ->relationship( - 'allocation', - 'ip', - fn (Builder $query, Forms\Get $get) => $query - ->where('node_id', $get('node_id')) - ->whereNull('server_id'), - ) - ->createOptionForm(fn (Forms\Get $get) => [ - Forms\Components\TextInput::make('allocation_ip') - ->datalist(Node::find($get('node_id'))?->ipAddresses() ?? []) - ->label('IP Address') - ->ipv4() - ->helperText("Usually your machine's public IP unless you are port forwarding.") - // ->selectablePlaceholder(false) - ->required(), - Forms\Components\TextInput::make('allocation_alias') - ->label('Alias') - ->default(null) - ->datalist([ - $get('name'), - Egg::find($get('egg_id'))?->name, - ]) - ->helperText('This is just a display only name to help you recognize what this Allocation is used for.') - ->required(false), - Forms\Components\TagsInput::make('allocation_ports') - ->placeholder('Examples: 27015, 27017-27019') - ->helperText(' - These are the ports that users can connect to this Server through. - They usually consist of the port forwarded ones. - ') - ->label('Ports') - ->live() - ->afterStateUpdated(function ($state, Forms\Set $set) { - $ports = collect(); - $update = false; - foreach ($state as $portEntry) { - if (!str_contains($portEntry, '-')) { - if (is_numeric($portEntry)) { - $ports->push((int) $portEntry); - - continue; - } - - // Do not add non numerical ports - $update = true; - - continue; - } - - $update = true; - [$start, $end] = explode('-', $portEntry); - if (!is_numeric($start) || !is_numeric($end)) { - continue; - } - - $start = max((int) $start, 0); - $end = min((int) $end, 2 ** 16 - 1); - for ($i = $start; $i <= $end; $i++) { - $ports->push($i); - } - } - - $uniquePorts = $ports->unique()->values(); - if ($ports->count() > $uniquePorts->count()) { - $update = true; - $ports = $uniquePorts; - } - - $sortedPorts = $ports->sort()->values(); - if ($sortedPorts->all() !== $ports->all()) { - $update = true; - $ports = $sortedPorts; - } - - if ($update) { - $set('allocation_ports', $ports->all()); - } - }) - ->splitKeys(['Tab', ' ', ',']) - ->required(), - ]) - ->createOptionUsing(function (array $data, Forms\Get $get): int { - return collect( - resolve(AssignmentService::class)->handle(Node::find($get('node_id')), $data) - )->first(); - }) - ->required(), - - Forms\Components\Repeater::make('allocation_additional') - ->label('Additional Allocations') - ->columnSpan(2) - ->addActionLabel('Add Allocation') - ->disabled(fn (Forms\Get $get) => $get('allocation_id') === null) - // ->addable() TODO disable when all allocations are taken - // ->addable() TODO disable until first additional allocation is selected - ->simple( - Forms\Components\Select::make('extra_allocations') - ->live() - ->preload() - ->disableOptionsWhenSelectedInSiblingRepeaterItems() - ->prefixIcon('tabler-network') - ->label('Additional Allocations') - ->columnSpan(2) - ->disabled(fn (Forms\Get $get) => $get('../../node_id') === null) - ->searchable(['ip', 'port', 'ip_alias']) - ->getOptionLabelFromRecordUsing( - fn (Allocation $allocation) => "$allocation->ip:$allocation->port" . - ($allocation->ip_alias ? " ($allocation->ip_alias)" : '') - ) - ->placeholder('Select additional Allocations') - ->relationship( - 'allocations', - 'ip', - fn (Builder $query, Forms\Get $get, Forms\Components\Select $component, $state) => $query - ->where('node_id', $get('../../node_id')) - ->whereNotIn( - 'id', - collect(($repeater = $component->getParentRepeater())->getState()) - ->pluck( - (string) str($component->getStatePath()) - ->after("{$repeater->getStatePath()}.") - ->after('.'), - ) - ->flatten() - ->diff(Arr::wrap($state)) - ->filter(fn (mixed $siblingItemState): bool => filled($siblingItemState)) - ->add($get('../../allocation_id')) - ) - ->whereNull('server_id'), - ), - ), - - Forms\Components\Textarea::make('description') - ->hidden() - ->default('') - ->required() - ->columnSpanFull(), - - Forms\Components\Select::make('egg_id') - ->disabledOn('edit') - ->prefixIcon('tabler-egg') - ->columnSpan([ - 'default' => 2, - 'sm' => 2, - 'md' => 2, - 'lg' => 6, - ]) - ->relationship('egg', 'name') - ->searchable() - ->preload() - ->live() - ->afterStateUpdated(function ($state, Forms\Set $set) { - $egg = Egg::find($state); - $set('startup', $egg->startup); - - $variables = $egg->variables ?? []; - $serverVariables = collect(); - foreach ($variables as $variable) { - $serverVariables->add($variable->toArray()); - } - - $variables = []; - $set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all()); - for ($i = 0; $i < $serverVariables->count(); $i++) { - $set("$path.$i.variable_value", $serverVariables[$i]['default_value']); - $set("$path.$i.variable_id", $serverVariables[$i]['id']); - $variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value']; - } - - $set('environment', $variables); - }) - ->required(), - - Forms\Components\ToggleButtons::make('skip_scripts') - ->label('Run Egg Install Script?') - ->default(false) - ->options([ - false => 'Yes', - true => 'Skip', - ]) - ->colors([ - false => 'primary', - true => 'danger', - ]) - ->icons([ - false => 'tabler-code', - true => 'tabler-code-off', - ]) - ->inline() - ->required(), - - Forms\Components\ToggleButtons::make('custom_image') - ->live() - ->label('Custom Image?') - ->default(false) - ->formatStateUsing(function ($state, Forms\Get $get) { - if ($state !== null) { - return $state; - } - - $images = Egg::find($get('egg_id'))->docker_images ?? []; - - return !in_array($get('image'), $images); - }) - ->options([ - false => 'No', - true => 'Yes', - ]) - ->colors([ - false => 'primary', - true => 'danger', - ]) - ->icons([ - false => 'tabler-settings-cancel', - true => 'tabler-settings-check', - ]) - ->inline(), - - Forms\Components\TextInput::make('image') - ->hidden(fn (Forms\Get $get) => !$get('custom_image')) - ->disabled(fn (Forms\Get $get) => !$get('custom_image')) - ->label('Docker Image') - ->placeholder('Enter a custom Image') - ->columnSpan([ - 'default' => 2, - 'sm' => 2, - 'md' => 2, - 'lg' => 4, - ]) - ->required(), - - Forms\Components\Select::make('image') - ->hidden(fn (Forms\Get $get) => $get('custom_image')) - ->disabled(fn (Forms\Get $get) => $get('custom_image')) - ->label('Docker Image') - ->prefixIcon('tabler-brand-docker') - ->options(function (Forms\Get $get, Forms\Set $set) { - $images = Egg::find($get('egg_id'))->docker_images ?? []; - - $set('image', collect($images)->first()); - - return $images; - }) - ->disabled(fn (Forms\Components\Select $component) => empty($component->getOptions())) - ->selectablePlaceholder(false) - ->columnSpan([ - 'default' => 2, - 'sm' => 2, - 'md' => 2, - 'lg' => 4, - ]) - ->required(), - - Forms\Components\Fieldset::make('Application Feature Limits') - ->inlineLabel() - ->hiddenOn('create') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 3, - ]) - ->schema([ - Forms\Components\TextInput::make('allocation_limit') - ->suffixIcon('tabler-network') - ->required() - ->numeric() - ->default(0), - Forms\Components\TextInput::make('database_limit') - ->suffixIcon('tabler-database') - ->required() - ->numeric() - ->default(0), - Forms\Components\TextInput::make('backup_limit') - ->suffixIcon('tabler-copy-check') - ->required() - ->numeric() - ->default(0), - ]), - - Forms\Components\Textarea::make('startup') - ->hintIcon('tabler-code') - ->label('Startup Command') - ->required() - ->live() - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) - ->rows(function ($state) { - return str($state)->explode("\n")->reduce( - fn (int $carry, $line) => $carry + floor(strlen($line) / 125), - 0 - ); - }), - - Forms\Components\Hidden::make('environment')->default([]), - - Forms\Components\Hidden::make('start_on_completion')->default(true), - - Forms\Components\Section::make('Egg Variables') - ->icon('tabler-eggs') - ->iconColor('primary') - ->collapsible() - ->collapsed() - ->columnSpan(([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ])) - ->schema([ - Forms\Components\Placeholder::make('Select an egg first to show its variables!') - ->hidden(fn (Forms\Get $get) => !empty($get('server_variables'))), - - Forms\Components\Repeater::make('server_variables') - ->relationship('serverVariables') - ->grid() - ->deletable(false) - ->default([]) - ->hidden(fn ($state) => empty($state)) - ->schema([ - Forms\Components\TextInput::make('variable_value') - ->rules([ - fn (ServerVariable $variable): Closure => function (string $attribute, $value, Closure $fail) use ($variable) { - $validator = Validator::make(['validatorkey' => $value], [ - 'validatorkey' => $variable->variable->rules, - ]); - - if ($validator->fails()) { - $message = str($validator->errors()->first())->replace('validatorkey', $variable->variable->name); - - $fail($message); - } - }, - ]) - ->label(fn (ServerVariable $variable) => $variable->variable->name) - ->hintIcon('tabler-code') - ->hintIconTooltip(fn (ServerVariable $variable) => $variable->variable->rules) - ->prefix(fn (ServerVariable $variable) => '{{' . $variable->variable->env_variable . '}}') - ->helperText(fn (ServerVariable $variable) => $variable->variable->description ?: '—') - ->maxLength(191), - - Forms\Components\Hidden::make('variable_id')->default(0), - ]) - ->columnSpan(2), - ]), - - Forms\Components\Section::make('Resource Management') - // ->hiddenOn('create') - ->collapsed() - ->icon('tabler-server-cog') - ->iconColor('primary') - ->columns(2) - ->columnSpan(([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ])) - ->schema([ - Forms\Components\TextInput::make('memory') - ->default(0) - ->label('Allocated Memory') - ->suffix('MB') - ->required() - ->numeric(), - - Forms\Components\TextInput::make('swap') - ->default(0) - ->label('Swap Memory') - ->suffix('MB') - ->helperText('0 disables swap and -1 allows unlimited swap') - ->minValue(-1) - ->required() - ->numeric(), - - Forms\Components\TextInput::make('disk') - ->default(0) - ->label('Disk Space Limit') - ->suffix('MB') - ->required() - ->numeric(), - - Forms\Components\TextInput::make('cpu') - ->default(0) - ->label('CPU Limit') - ->suffix('%') - ->required() - ->numeric(), - - Forms\Components\TextInput::make('threads') - ->hint('Advanced') - ->hintColor('danger') - ->helperText('Examples: 0, 0-1,3, or 0,1,3,4') - ->label('CPU Pinning') - ->suffixIcon('tabler-cpu') - ->maxLength(191), - - Forms\Components\TextInput::make('io') - ->helperText('The IO performance relative to other running containers') - ->label('Block IO Proportion') - ->required() - ->minValue(0) - ->maxValue(1000) - ->step(10) - ->default(0) - ->numeric(), - - Forms\Components\ToggleButtons::make('oom_disabled') - ->label('OOM Killer') - ->inline() - ->default(false) - ->options([ - false => 'Disabled', - true => 'Enabled', - ]) - ->colors([ - false => 'success', - true => 'danger', - ]) - ->icons([ - false => 'tabler-sword-off', - true => 'tabler-sword', - ]) - ->required(), - ]), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->searchable(false) - ->columns([ - Tables\Columns\TextColumn::make('status') - ->default('unknown') - ->badge() - ->default(function (Server $server) { - if ($server->status !== null) { - return $server->status; - } - - $statuses = collect($server->retrieveStatus()) - ->mapWithKeys(function ($status) { - return [$status['configuration']['uuid'] => $status['state']]; - })->all(); - - return $statuses[$server->uuid] ?? 'node_fail'; - }) - ->icon(fn ($state) => match ($state) { - 'node_fail' => 'tabler-server-off', - 'running' => 'tabler-heartbeat', - 'removing' => 'tabler-heart-x', - 'offline' => 'tabler-heart-off', - 'paused' => 'tabler-heart-pause', - 'installing' => 'tabler-heart-bolt', - 'suspended' => 'tabler-heart-cancel', - default => 'tabler-heart-question', - }) - ->color(fn ($state): string => match ($state) { - 'running' => 'success', - 'installing', 'restarting' => 'primary', - 'paused', 'removing' => 'warning', - 'node_fail', 'install_failed', 'suspended' => 'danger', - default => 'gray', - }), - - Tables\Columns\TextColumn::make('uuid') - ->hidden() - ->label('UUID') - ->searchable(), - Tables\Columns\TextColumn::make('name') - ->icon('tabler-brand-docker') - ->searchable() - ->sortable(), - Tables\Columns\TextColumn::make('node.name') - ->icon('tabler-server-2') - ->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])) - ->sortable(), - Tables\Columns\TextColumn::make('egg.name') - ->icon('tabler-egg') - ->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg])) - ->sortable(), - Tables\Columns\TextColumn::make('user.username') - ->icon('tabler-user') - ->label('Owner') - ->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user])) - ->sortable(), - Tables\Columns\SelectColumn::make('allocation_id') - ->label('Primary Allocation') - ->options(fn ($state, Server $server) => $server->allocations->mapWithKeys( - fn ($allocation) => [$allocation->id => $allocation->address]) - ) - ->selectablePlaceholder(false) - ->sortable(), - Tables\Columns\TextColumn::make('image')->hidden(), - Tables\Columns\TextColumn::make('backups_count') - ->counts('backups') - ->label('Backups') - ->icon('tabler-file-download') - ->numeric() - ->sortable(), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - // Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - public static function getRelations(): array { return [ diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index 2fd299501..d3a347c32 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -3,15 +3,590 @@ namespace App\Filament\Resources\ServerResource\Pages; use App\Filament\Resources\ServerResource; -use App\Models\Server; -use App\Services\Servers\ServerDeletionService; use Filament\Actions; +use Filament\Forms; +use App\Enums\ContainerStatus; +use App\Enums\ServerState; +use App\Models\Allocation; +use App\Models\Egg; +use App\Models\Node; +use App\Models\Server; +use App\Models\ServerVariable; +use App\Repositories\Daemon\DaemonServerRepository; +use App\Services\Allocations\AssignmentService; +use App\Services\Servers\ServerDeletionService; +use Filament\Forms\Form; use Filament\Resources\Pages\EditRecord; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Validator; +use Closure; class EditServer extends EditRecord { protected static string $resource = ServerResource::class; + public function form(Form $form): Form + { + return $form + ->columns([ + 'default' => 2, + 'sm' => 2, + 'md' => 4, + 'lg' => 6, + ]) + ->schema([ + Forms\Components\ToggleButtons::make('docker') + ->label('Container Status') + ->hiddenOn('create') + ->inlineLabel() + ->formatStateUsing(function ($state, Server $server) { + if ($server->node_id === null) { + return 'unknown'; + } + + /** @var DaemonServerRepository $service */ + $service = resolve(DaemonServerRepository::class); + $details = $service->setServer($server)->getDetails(); + + return $details['state'] ?? 'unknown'; + }) + ->options(fn ($state) => collect(ContainerStatus::cases())->filter(fn ($containerStatus) => $containerStatus->value === $state)->mapWithKeys( + fn (ContainerStatus $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()] + )) + ->colors(collect(ContainerStatus::cases())->mapWithKeys( + fn (ContainerStatus $status) => [$status->value => $status->color()] + )) + ->icons(collect(ContainerStatus::cases())->mapWithKeys( + fn (ContainerStatus $status) => [$status->value => $status->icon()] + )) + ->columnSpan([ + 'default' => 1, + 'sm' => 2, + 'md' => 2, + 'lg' => 3, + ]) + ->inline(), + + Forms\Components\ToggleButtons::make('status') + ->label('Server State') + ->helperText('') + ->hiddenOn('create') + ->inlineLabel() + ->formatStateUsing(fn ($state) => $state ?? ServerState::Normal) + ->options(fn ($state) => collect(ServerState::cases())->filter(fn ($serverState) => $serverState->value === $state)->mapWithKeys( + fn (ServerState $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()] + )) + ->colors(collect(ServerState::cases())->mapWithKeys( + fn (ServerState $state) => [$state->value => $state->color()] + )) + ->icons(collect(ServerState::cases())->mapWithKeys( + fn (ServerState $state) => [$state->value => $state->icon()] + )) + ->columnSpan([ + 'default' => 1, + 'sm' => 2, + 'md' => 2, + 'lg' => 3, + ]) + ->inline(), + + Forms\Components\TextInput::make('external_id') + ->maxLength(191) + ->hidden(), + + Forms\Components\TextInput::make('name') + ->prefixIcon('tabler-server') + ->label('Display Name') + ->suffixAction(Forms\Components\Actions\Action::make('random') + ->icon('tabler-dice-' . random_int(1, 6)) + ->color('primary') + ->action(function (Forms\Set $set, Forms\Get $get) { + $egg = Egg::find($get('egg_id')); + $prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : ''; + + $set('name', $prefix . fake()->domainWord); + })) + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 2, + 'lg' => 3, + ]) + ->required() + ->maxLength(191), + + Forms\Components\Select::make('owner_id') + ->prefixIcon('tabler-user') + ->default(auth()->user()->id) + ->label('Owner') + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 2, + 'lg' => 3, + ]) + ->relationship('user', 'username') + ->searchable() + ->preload() + ->required(), + + Forms\Components\Select::make('node_id') + ->disabledOn('edit') + ->prefixIcon('tabler-server-2') + ->default(fn () => Node::query()->latest()->first()?->id) + ->columnSpan(2) + ->live() + ->relationship('node', 'name') + ->searchable() + ->preload() + ->afterStateUpdated(fn (Forms\Set $set) => $set('allocation_id', null)) + ->required(), + + Forms\Components\Select::make('allocation_id') + ->preload() + ->live() + ->prefixIcon('tabler-network') + ->label('Primary Allocation') + ->columnSpan(2) + ->disabled(fn (Forms\Get $get) => $get('node_id') === null) + ->searchable(['ip', 'port', 'ip_alias']) + ->afterStateUpdated(function (Forms\Set $set) { + $set('allocation_additional', null); + $set('allocation_additional.needstobeastringhere.extra_allocations', null); + }) + ->getOptionLabelFromRecordUsing( + fn (Allocation $allocation) => "$allocation->ip:$allocation->port" . + ($allocation->ip_alias ? " ($allocation->ip_alias)" : '') + ) + ->placeholder(function (Forms\Get $get) { + $node = Node::find($get('node_id')); + + if ($node?->allocations) { + return 'Select an Allocation'; + } + + return 'Create a New Allocation'; + }) + ->relationship( + 'allocation', + 'ip', + fn (Builder $query, Forms\Get $get) => $query + ->where('node_id', $get('node_id')) + ->whereNull('server_id'), + ) + ->createOptionForm(fn (Forms\Get $get) => [ + Forms\Components\TextInput::make('allocation_ip') + ->datalist(Node::find($get('node_id'))?->ipAddresses() ?? []) + ->label('IP Address') + ->ipv4() + ->helperText("Usually your machine's public IP unless you are port forwarding.") + // ->selectablePlaceholder(false) + ->required(), + Forms\Components\TextInput::make('allocation_alias') + ->label('Alias') + ->default(null) + ->datalist([ + $get('name'), + Egg::find($get('egg_id'))?->name, + ]) + ->helperText('This is just a display only name to help you recognize what this Allocation is used for.') + ->required(false), + Forms\Components\TagsInput::make('allocation_ports') + ->placeholder('Examples: 27015, 27017-27019') + ->helperText(' + These are the ports that users can connect to this Server through. + They usually consist of the port forwarded ones. + ') + ->label('Ports') + ->live() + ->afterStateUpdated(function ($state, Forms\Set $set) { + $ports = collect(); + $update = false; + foreach ($state as $portEntry) { + if (!str_contains($portEntry, '-')) { + if (is_numeric($portEntry)) { + $ports->push((int) $portEntry); + + continue; + } + + // Do not add non numerical ports + $update = true; + + continue; + } + + $update = true; + [$start, $end] = explode('-', $portEntry); + if (!is_numeric($start) || !is_numeric($end)) { + continue; + } + + $start = max((int) $start, 0); + $end = min((int) $end, 2 ** 16 - 1); + for ($i = $start; $i <= $end; $i++) { + $ports->push($i); + } + } + + $uniquePorts = $ports->unique()->values(); + if ($ports->count() > $uniquePorts->count()) { + $update = true; + $ports = $uniquePorts; + } + + $sortedPorts = $ports->sort()->values(); + if ($sortedPorts->all() !== $ports->all()) { + $update = true; + $ports = $sortedPorts; + } + + if ($update) { + $set('allocation_ports', $ports->all()); + } + }) + ->splitKeys(['Tab', ' ', ',']) + ->required(), + ]) + ->createOptionUsing(function (array $data, Forms\Get $get): int { + return collect( + resolve(AssignmentService::class)->handle(Node::find($get('node_id')), $data) + )->first(); + }) + ->required(), + + Forms\Components\Repeater::make('allocation_additional') + ->label('Additional Allocations') + ->columnSpan(2) + ->addActionLabel('Add Allocation') + ->disabled(fn (Forms\Get $get) => $get('allocation_id') === null) + // ->addable() TODO disable when all allocations are taken + // ->addable() TODO disable until first additional allocation is selected + ->simple( + Forms\Components\Select::make('extra_allocations') + ->live() + ->preload() + ->disableOptionsWhenSelectedInSiblingRepeaterItems() + ->prefixIcon('tabler-network') + ->label('Additional Allocations') + ->columnSpan(2) + ->disabled(fn (Forms\Get $get) => $get('../../node_id') === null) + ->searchable(['ip', 'port', 'ip_alias']) + ->getOptionLabelFromRecordUsing( + fn (Allocation $allocation) => "$allocation->ip:$allocation->port" . + ($allocation->ip_alias ? " ($allocation->ip_alias)" : '') + ) + ->placeholder('Select additional Allocations') + ->relationship( + 'allocations', + 'ip', + fn (Builder $query, Forms\Get $get, Forms\Components\Select $component, $state) => $query + ->where('node_id', $get('../../node_id')) + ->whereNotIn( + 'id', + collect(($repeater = $component->getParentRepeater())->getState()) + ->pluck( + (string) str($component->getStatePath()) + ->after("{$repeater->getStatePath()}.") + ->after('.'), + ) + ->flatten() + ->diff(Arr::wrap($state)) + ->filter(fn (mixed $siblingItemState): bool => filled($siblingItemState)) + ->add($get('../../allocation_id')) + ) + ->whereNull('server_id'), + ), + ), + + Forms\Components\Textarea::make('description') + ->hidden() + ->default('') + ->required() + ->columnSpanFull(), + + Forms\Components\Select::make('egg_id') + ->disabledOn('edit') + ->prefixIcon('tabler-egg') + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 6, + ]) + ->relationship('egg', 'name') + ->searchable() + ->preload() + ->required(), + + Forms\Components\ToggleButtons::make('skip_scripts') + ->label('Run Egg Install Script?') + ->default(false) + ->options([ + false => 'Yes', + true => 'Skip', + ]) + ->colors([ + false => 'primary', + true => 'danger', + ]) + ->icons([ + false => 'tabler-code', + true => 'tabler-code-off', + ]) + ->inline() + ->required(), + + Forms\Components\ToggleButtons::make('custom_image') + ->live() + ->label('Custom Image?') + ->default(false) + ->formatStateUsing(function ($state, Forms\Get $get) { + if ($state !== null) { + return $state; + } + + $images = Egg::find($get('egg_id'))->docker_images ?? []; + + return !in_array($get('image'), $images); + }) + ->options([ + false => 'No', + true => 'Yes', + ]) + ->colors([ + false => 'primary', + true => 'danger', + ]) + ->icons([ + false => 'tabler-settings-cancel', + true => 'tabler-settings-check', + ]) + ->inline(), + + Forms\Components\TextInput::make('image') + ->hidden(fn (Forms\Get $get) => !$get('custom_image')) + ->disabled(fn (Forms\Get $get) => !$get('custom_image')) + ->label('Docker Image') + ->placeholder('Enter a custom Image') + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 4, + ]) + ->required(), + + Forms\Components\Select::make('image') + ->hidden(fn (Forms\Get $get) => $get('custom_image')) + ->disabled(fn (Forms\Get $get) => $get('custom_image')) + ->label('Docker Image') + ->prefixIcon('tabler-brand-docker') + ->options(function (Forms\Get $get, Forms\Set $set) { + $images = Egg::find($get('egg_id'))->docker_images ?? []; + + $set('image', collect($images)->first()); + + return $images; + }) + ->disabled(fn (Forms\Components\Select $component) => empty($component->getOptions())) + ->selectablePlaceholder(false) + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 4, + ]) + ->required(), + + Forms\Components\Fieldset::make('Application Feature Limits') + ->inlineLabel() + ->hiddenOn('create') + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ]) + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 3, + ]) + ->schema([ + Forms\Components\TextInput::make('allocation_limit') + ->suffixIcon('tabler-network') + ->required() + ->numeric() + ->default(0), + Forms\Components\TextInput::make('database_limit') + ->suffixIcon('tabler-database') + ->required() + ->numeric() + ->default(0), + Forms\Components\TextInput::make('backup_limit') + ->suffixIcon('tabler-copy-check') + ->required() + ->numeric() + ->default(0), + ]), + + Forms\Components\Textarea::make('startup') + ->hintIcon('tabler-code') + ->label('Startup Command') + ->required() + ->live() + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ]) + ->rows(function ($state) { + return str($state)->explode("\n")->reduce( + fn (int $carry, $line) => $carry + floor(strlen($line) / 125), + 0 + ); + }), + + Forms\Components\Hidden::make('environment')->default([]), + + Forms\Components\Hidden::make('start_on_completion')->default(true), + + Forms\Components\Section::make('Egg Variables') + ->icon('tabler-eggs') + ->iconColor('primary') + ->collapsible() + ->collapsed() + ->columnSpan(([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ])) + ->schema([ + Forms\Components\Placeholder::make('Select an egg first to show its variables!') + ->hidden(fn (Forms\Get $get) => !empty($get('server_variables'))), + + Forms\Components\Repeater::make('server_variables') + ->relationship('serverVariables', fn ($query) => $query + ->join('egg_variables', 'egg_variables.id', '=', 'server_variables.variable_id') + ->orderBy('egg_variables.sort') + ) + ->grid() + ->deletable(false) + ->default([]) + ->hidden(fn ($state) => empty($state)) + ->schema([ + Forms\Components\TextInput::make('variable_value') + ->rules([ + fn (ServerVariable $variable): Closure => function (string $attribute, $value, Closure $fail) use ($variable) { + $validator = Validator::make(['validatorkey' => $value], [ + 'validatorkey' => $variable->variable->rules, + ]); + + if ($validator->fails()) { + $message = str($validator->errors()->first())->replace('validatorkey', $variable->variable->name); + + $fail($message); + } + }, + ]) + ->label(fn (ServerVariable $variable) => $variable->variable->name) + ->hintIcon('tabler-code') + ->hintIconTooltip(fn (ServerVariable $variable) => $variable->variable->rules) + ->prefix(fn (ServerVariable $variable) => '{{' . $variable->variable->env_variable . '}}') + ->helperText(fn (ServerVariable $variable) => $variable->variable->description ?: '—') + ->maxLength(191), + + Forms\Components\Hidden::make('variable_id')->default(0), + ]) + ->columnSpan(2), + ]), + + Forms\Components\Section::make('Resource Management') + // ->hiddenOn('create') + ->collapsed() + ->icon('tabler-server-cog') + ->iconColor('primary') + ->columns(2) + ->columnSpan(([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ])) + ->schema([ + Forms\Components\TextInput::make('memory') + ->default(0) + ->label('Allocated Memory') + ->suffix('MB') + ->required() + ->numeric(), + + Forms\Components\TextInput::make('swap') + ->default(0) + ->label('Swap Memory') + ->suffix('MB') + ->helperText('0 disables swap and -1 allows unlimited swap') + ->minValue(-1) + ->required() + ->numeric(), + + Forms\Components\TextInput::make('disk') + ->default(0) + ->label('Disk Space Limit') + ->suffix('MB') + ->required() + ->numeric(), + + Forms\Components\TextInput::make('cpu') + ->default(0) + ->label('CPU Limit') + ->suffix('%') + ->required() + ->numeric(), + + Forms\Components\TextInput::make('threads') + ->hint('Advanced') + ->hintColor('danger') + ->helperText('Examples: 0, 0-1,3, or 0,1,3,4') + ->label('CPU Pinning') + ->suffixIcon('tabler-cpu') + ->maxLength(191), + + Forms\Components\TextInput::make('io') + ->helperText('The IO performance relative to other running containers') + ->label('Block IO Proportion') + ->required() + ->minValue(0) + ->maxValue(1000) + ->step(10) + ->default(0) + ->numeric(), + + Forms\Components\ToggleButtons::make('oom_disabled') + ->label('OOM Killer') + ->inline() + ->default(false) + ->options([ + false => 'Disabled', + true => 'Enabled', + ]) + ->colors([ + false => 'success', + true => 'danger', + ]) + ->icons([ + false => 'tabler-sword-off', + true => 'tabler-sword', + ]) + ->required(), + ]), + ]); + } protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/ServerResource/Pages/ListServers.php b/app/Filament/Resources/ServerResource/Pages/ListServers.php index be913c5cf..daba3badf 100644 --- a/app/Filament/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/Resources/ServerResource/Pages/ListServers.php @@ -3,13 +3,102 @@ namespace App\Filament\Resources\ServerResource\Pages; use App\Filament\Resources\ServerResource; +use App\Models\Server; use Filament\Actions; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Table; +use Filament\Tables; class ListServers extends ListRecords { protected static string $resource = ServerResource::class; + public function table(Table $table): Table + { + return $table + ->searchable(false) + ->columns([ + Tables\Columns\TextColumn::make('status') + ->default('unknown') + ->badge() + ->default(function (Server $server) { + if ($server->status !== null) { + return $server->status; + } + + $statuses = collect($server->retrieveStatus()) + ->mapWithKeys(function ($status) { + return [$status['configuration']['uuid'] => $status['state']]; + })->all(); + + return $statuses[$server->uuid] ?? 'node_fail'; + }) + ->icon(fn ($state) => match ($state) { + 'node_fail' => 'tabler-server-off', + 'running' => 'tabler-heartbeat', + 'removing' => 'tabler-heart-x', + 'offline' => 'tabler-heart-off', + 'paused' => 'tabler-heart-pause', + 'installing' => 'tabler-heart-bolt', + 'suspended' => 'tabler-heart-cancel', + default => 'tabler-heart-question', + }) + ->color(fn ($state): string => match ($state) { + 'running' => 'success', + 'installing', 'restarting' => 'primary', + 'paused', 'removing' => 'warning', + 'node_fail', 'install_failed', 'suspended' => 'danger', + default => 'gray', + }), + + Tables\Columns\TextColumn::make('uuid') + ->hidden() + ->label('UUID') + ->searchable(), + Tables\Columns\TextColumn::make('name') + ->icon('tabler-brand-docker') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('node.name') + ->icon('tabler-server-2') + ->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])) + ->sortable(), + Tables\Columns\TextColumn::make('egg.name') + ->icon('tabler-egg') + ->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg])) + ->sortable(), + Tables\Columns\TextColumn::make('user.username') + ->icon('tabler-user') + ->label('Owner') + ->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user])) + ->sortable(), + Tables\Columns\SelectColumn::make('allocation_id') + ->label('Primary Allocation') + ->options(fn ($state, Server $server) => $server->allocations->mapWithKeys( + fn ($allocation) => [$allocation->id => $allocation->address]) + ) + ->selectablePlaceholder(false) + ->sortable(), + Tables\Columns\TextColumn::make('image')->hidden(), + Tables\Columns\TextColumn::make('backups_count') + ->counts('backups') + ->label('Backups') + ->icon('tabler-file-download') + ->numeric() + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + // Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index a2b55cc7b..e8e3a466e 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -5,13 +5,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource\RelationManagers; use App\Models\User; -use Filament\Forms; -use Filament\Forms\Components\Section; -use Filament\Forms\Form; use Filament\Resources\Resource; -use Filament\Tables; -use Filament\Tables\Table; -use Illuminate\Support\Facades\Hash; class UserResource extends Resource { @@ -21,120 +15,6 @@ class UserResource extends Resource protected static ?string $recordTitleAttribute = 'username'; - public static function form(Form $form): Form - { - return $form - ->schema([ - Section::make()->schema([ - Forms\Components\TextInput::make('username')->required()->maxLength(191), - Forms\Components\TextInput::make('email')->email()->required()->maxLength(191), - - Forms\Components\TextInput::make('name_first') - ->maxLength(191) - ->hidden(fn (string $operation): bool => $operation === 'create') - ->label('First Name'), - Forms\Components\TextInput::make('name_last') - ->maxLength(191) - ->hidden(fn (string $operation): bool => $operation === 'create') - ->label('Last Name'), - - Forms\Components\TextInput::make('password') - ->dehydrateStateUsing(fn (string $state): string => Hash::make($state)) - ->dehydrated(fn (?string $state): bool => filled($state)) - ->required(fn (string $operation): bool => $operation === 'create') - ->password(), - - Forms\Components\ToggleButtons::make('root_admin') - ->label('Administrator (Root)') - ->options([ - false => 'No', - true => 'Admin', - ]) - ->colors([ - false => 'primary', - true => 'danger', - ]) - ->disableOptionWhen(function (string $operation, $value, User $user) { - if ($operation !== 'edit' || $value) { - return false; - } - - return $user->isLastRootAdmin(); - }) - ->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '') - ->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '') - ->hintColor('warning') - ->inline() - ->required() - ->default(false), - - Forms\Components\Hidden::make('skipValidation')->default(true), - Forms\Components\Select::make('language') - ->required() - ->hidden() - ->default('en') - ->options(fn (User $user) => $user->getAvailableLanguages()), - ])->columns(2), - ]); - } - - public static function table(Table $table): Table - { - return $table - ->searchable(false) - ->columns([ - Tables\Columns\ImageColumn::make('picture') - ->visibleFrom('lg') - ->label('') - ->extraImgAttributes(['class' => 'rounded-full']) - ->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))), - Tables\Columns\TextColumn::make('external_id') - ->searchable() - ->hidden(), - Tables\Columns\TextColumn::make('uuid') - ->label('UUID') - ->hidden() - ->searchable(), - Tables\Columns\TextColumn::make('username') - ->searchable(), - Tables\Columns\TextColumn::make('email') - ->searchable() - ->icon('tabler-mail'), - Tables\Columns\IconColumn::make('root_admin') - ->visibleFrom('md') - ->label('Admin') - ->boolean() - ->trueIcon('tabler-star') - ->falseIcon('tabler-star-off') - ->sortable(), - Tables\Columns\IconColumn::make('use_totp')->label('2FA') - ->visibleFrom('lg') - ->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off') - ->boolean()->sortable(), - Tables\Columns\TextColumn::make('servers_count') - ->counts('servers') - ->icon('tabler-server') - ->label('Servers'), - Tables\Columns\TextColumn::make('subusers_count') - ->visibleFrom('sm') - ->counts('subusers') - ->icon('tabler-users') - // ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count)) - ->label('Subuser Accounts'), - ]) - ->filters([ - // - ]) - ->actions([ - Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), - ]); - } - public static function getRelations(): array { return [ diff --git a/app/Filament/Resources/UserResource/Pages/CreateUser.php b/app/Filament/Resources/UserResource/Pages/CreateUser.php index 78a389490..20d4397ae 100644 --- a/app/Filament/Resources/UserResource/Pages/CreateUser.php +++ b/app/Filament/Resources/UserResource/Pages/CreateUser.php @@ -4,8 +4,70 @@ namespace App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource; use Filament\Resources\Pages\CreateRecord; +use App\Models\User; +use Filament\Forms; +use Filament\Forms\Components\Section; +use Filament\Forms\Form; +use Illuminate\Support\Facades\Hash; class CreateUser extends CreateRecord { protected static string $resource = UserResource::class; + + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make()->schema([ + Forms\Components\TextInput::make('username')->required()->maxLength(191), + Forms\Components\TextInput::make('email')->email()->required()->maxLength(191), + + Forms\Components\TextInput::make('name_first') + ->maxLength(191) + ->hidden(fn (string $operation): bool => $operation === 'create') + ->label('First Name'), + Forms\Components\TextInput::make('name_last') + ->maxLength(191) + ->hidden(fn (string $operation): bool => $operation === 'create') + ->label('Last Name'), + + Forms\Components\TextInput::make('password') + ->dehydrateStateUsing(fn (string $state): string => Hash::make($state)) + ->dehydrated(fn (?string $state): bool => filled($state)) + ->required(fn (string $operation): bool => $operation === 'create') + ->password(), + + Forms\Components\ToggleButtons::make('root_admin') + ->label('Administrator (Root)') + ->options([ + false => 'No', + true => 'Admin', + ]) + ->colors([ + false => 'primary', + true => 'danger', + ]) + ->disableOptionWhen(function (string $operation, $value, User $user) { + if ($operation !== 'edit' || $value) { + return false; + } + + return $user->isLastRootAdmin(); + }) + ->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '') + ->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '') + ->hintColor('warning') + ->inline() + ->required() + ->default(false), + + Forms\Components\Hidden::make('skipValidation')->default(true), + Forms\Components\Select::make('language') + ->required() + ->hidden() + ->default('en') + ->options(fn (User $user) => $user->getAvailableLanguages()), + ])->columns(2), + ]); + } } diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index b8770a937..9c4aa23d5 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -5,11 +5,71 @@ namespace App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource; use Filament\Actions; use Filament\Resources\Pages\EditRecord; +use App\Models\User; +use Filament\Forms; +use Filament\Forms\Components\Section; +use Filament\Forms\Form; +use Illuminate\Support\Facades\Hash; class EditUser extends EditRecord { protected static string $resource = UserResource::class; + public function form(Form $form): Form + { + return $form + ->schema([ + Section::make()->schema([ + Forms\Components\TextInput::make('username')->required()->maxLength(191), + Forms\Components\TextInput::make('email')->email()->required()->maxLength(191), + Forms\Components\TextInput::make('name_first') + ->maxLength(191) + ->hidden(fn (string $operation): bool => $operation === 'create') + ->label('First Name'), + Forms\Components\TextInput::make('name_last') + ->maxLength(191) + ->hidden(fn (string $operation): bool => $operation === 'create') + ->label('Last Name'), + + Forms\Components\TextInput::make('password') + ->dehydrateStateUsing(fn (string $state): string => Hash::make($state)) + ->dehydrated(fn (?string $state): bool => filled($state)) + ->required(fn (string $operation): bool => $operation === 'create') + ->password(), + + Forms\Components\ToggleButtons::make('root_admin') + ->label('Administrator (Root)') + ->options([ + false => 'No', + true => 'Admin', + ]) + ->colors([ + false => 'primary', + true => 'danger', + ]) + ->disableOptionWhen(function (string $operation, $value, User $user) { + if ($operation !== 'edit' || $value) { + return false; + } + + return $user->isLastRootAdmin(); + }) + ->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '') + ->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '') + ->hintColor('warning') + ->inline() + ->required() + ->default(false), + + Forms\Components\Hidden::make('skipValidation')->default(true), + Forms\Components\Select::make('language') + ->required() + ->hidden() + ->default('en') + ->options(fn (User $user) => $user->getAvailableLanguages()), + ])->columns(2), + ]); + } protected function getHeaderActions(): array { return [ diff --git a/app/Filament/Resources/UserResource/Pages/ListUsers.php b/app/Filament/Resources/UserResource/Pages/ListUsers.php index 0766ffed7..3af989413 100644 --- a/app/Filament/Resources/UserResource/Pages/ListUsers.php +++ b/app/Filament/Resources/UserResource/Pages/ListUsers.php @@ -3,13 +3,72 @@ namespace App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource; +use App\Models\User; use Filament\Actions; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Table; +use Filament\Tables; class ListUsers extends ListRecords { protected static string $resource = UserResource::class; + public function table(Table $table): Table + { + return $table + ->searchable(false) + ->columns([ + Tables\Columns\ImageColumn::make('picture') + ->visibleFrom('lg') + ->label('') + ->extraImgAttributes(['class' => 'rounded-full']) + ->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))), + Tables\Columns\TextColumn::make('external_id') + ->searchable() + ->hidden(), + Tables\Columns\TextColumn::make('uuid') + ->label('UUID') + ->hidden() + ->searchable(), + Tables\Columns\TextColumn::make('username') + ->searchable(), + Tables\Columns\TextColumn::make('email') + ->searchable() + ->icon('tabler-mail'), + Tables\Columns\IconColumn::make('root_admin') + ->visibleFrom('md') + ->label('Admin') + ->boolean() + ->trueIcon('tabler-star') + ->falseIcon('tabler-star-off') + ->sortable(), + Tables\Columns\IconColumn::make('use_totp')->label('2FA') + ->visibleFrom('lg') + ->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off') + ->boolean()->sortable(), + Tables\Columns\TextColumn::make('servers_count') + ->counts('servers') + ->icon('tabler-server') + ->label('Servers'), + Tables\Columns\TextColumn::make('subusers_count') + ->visibleFrom('sm') + ->counts('subusers') + ->icon('tabler-users') + // ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count)) + ->label('Subuser Accounts'), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } protected function getHeaderActions(): array { return [ diff --git a/app/Models/EggVariable.php b/app/Models/EggVariable.php index ca7daf1ff..568830a5b 100644 --- a/app/Models/EggVariable.php +++ b/app/Models/EggVariable.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $id * @property int $egg_id + * @property null $sort * @property string $name * @property string $description * @property string $env_variable @@ -50,6 +51,7 @@ class EggVariable extends Model public static array $validationRules = [ 'egg_id' => 'exists:eggs,id', + 'sort' => 'nullable', 'name' => 'required|string|between:1,191', 'description' => 'string', 'env_variable' => 'required|alphaDash|between:1,191|notIn:' . self::RESERVED_ENV_NAMES,