From dec1cf8e74ed8ad5bbdd7ee1d6d8fd3eb0aafa06 Mon Sep 17 00:00:00 2001 From: notCharles Date: Mon, 27 May 2024 20:02:13 -0400 Subject: [PATCH 1/5] Rework Edit Server Page a WIP, Also functional --- .../ServerResource/Pages/EditServer.php | 974 ++++++++++-------- .../ServerResource/Pages/EditServerOrg.php | 588 +++++++++++ .../Controllers/Admin/ServersController.php | 20 +- app/Services/Servers/SuspensionService.php | 6 +- 4 files changed, 1151 insertions(+), 437 deletions(-) create mode 100644 app/Filament/Resources/ServerResource/Pages/EditServerOrg.php diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index c047301a1..ef4ce86bf 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -3,7 +3,9 @@ namespace App\Filament\Resources\ServerResource\Pages; use App\Filament\Resources\ServerResource; +use App\Http\Controllers\Admin\ServersController; use App\Services\Servers\RandomWordService; +use App\Services\Servers\SuspensionService; use Filament\Actions; use Filament\Forms; use App\Enums\ContainerStatus; @@ -13,10 +15,12 @@ use App\Models\Server; use App\Models\ServerVariable; use App\Repositories\Daemon\DaemonServerRepository; use App\Services\Servers\ServerDeletionService; +use Filament\Forms\Components\Tabs; use Filament\Forms\Form; use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Facades\Validator; use Closure; +use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction; class EditServer extends EditRecord { @@ -25,12 +29,6 @@ class EditServer extends EditRecord 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')->inline()->inlineLabel() @@ -82,440 +80,554 @@ class EditServer extends EditRecord 'lg' => 3, ]), - 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)) - ->action(function (Forms\Set $set, Forms\Get $get) { - $egg = Egg::find($get('egg_id')); - $prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : ''; - - $word = (new RandomWordService())->word(); - - $set('name', $prefix . $word); - })) - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 2, - 'lg' => 3, - ]) - ->required() - ->maxLength(191), - - Forms\Components\Select::make('owner_id') - ->prefixIcon('tabler-user') - ->label('Owner') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 2, - 'lg' => 3, - ]) - ->relationship('user', 'username') - ->searchable() - ->preload() - ->required(), - - Forms\Components\Textarea::make('description') - ->hidden() - ->required() - ->columnSpanFull(), - - Forms\Components\Select::make('egg_id') - ->disabledOn('edit') - ->prefixIcon('tabler-egg') - ->columnSpan([ - 'default' => 2, - 'sm' => 2, - 'md' => 2, - 'lg' => 5, - ]) - ->relationship('egg', 'name') - ->searchable() - ->preload() - ->required(), - - Forms\Components\ToggleButtons::make('skip_scripts') - ->label('Run Egg Install Script?')->inline() - ->options([ - false => 'Yes', - true => 'Skip', - ]) - ->colors([ - false => 'primary', - true => 'danger', - ]) - ->icons([ - false => 'tabler-code', - true => 'tabler-code-off', - ]) - ->required(), - - 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('start_on_completion'), - - 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\Repeater::make('server_variables') - ->relationship('serverVariables') - ->grid() - ->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array { - foreach ($data as $key => $value) { - if (!isset($data['variable_value'])) { - $data['variable_value'] = ''; - } - } - - return $data; - }) - ->reorderable(false)->addable(false)->deletable(false) - ->schema(function () { - - $text = Forms\Components\TextInput::make('variable_value') - ->hidden($this->shouldHideComponent(...)) - ->maxLength(191) - ->rules([ - fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) { - $validator = Validator::make(['validatorkey' => $value], [ - 'validatorkey' => $serverVariable->variable->rules, - ]); - - if ($validator->fails()) { - $message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name); - - $fail($message); - } - }, - ]); - - $select = Forms\Components\Select::make('variable_value') - ->hidden($this->shouldHideComponent(...)) - ->options($this->getSelectOptionsFromRules(...)) - ->selectablePlaceholder(false); - - $components = [$text, $select]; - - /** @var Forms\Components\Component $component */ - foreach ($components as &$component) { - $component = $component - ->live(onBlur: true) - ->hintIcon('tabler-code') - ->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name) - ->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules) - ->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}') - ->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description); - } - - return $components; - }) - ->columnSpan(2), - ]), - - Forms\Components\Section::make('Environment Management') - ->collapsed() - ->icon('tabler-server-cog') - ->iconColor('primary') + Tabs::make('Tabs') + ->persistTabInQueryString() + ->columnSpan(6) ->columns([ 'default' => 2, - 'sm' => 4, + 'sm' => 2, 'md' => 4, - 'lg' => 4, + 'lg' => 6, ]) - ->columnSpanFull() - ->schema([ - Forms\Components\Fieldset::make('Resource Limits') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 3, - ]) + ->tabs([ + Tabs\Tab::make('Information') ->schema([ - Forms\Components\Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - Forms\Components\ToggleButtons::make('unlimited_mem') - ->label('Memory')->inlineLabel()->inline() - ->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0)) - ->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0) - ->live() - ->options([ - true => 'Unlimited', - false => 'Limited', - ]) - ->colors([ - true => 'primary', - false => 'warning', - ]) - ->columnSpan(2), + 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)) + ->action(function (Forms\Set $set, Forms\Get $get) { + $egg = Egg::find($get('egg_id')); + $prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : ''; - Forms\Components\TextInput::make('memory') - ->dehydratedWhenHidden() - ->hidden(fn (Forms\Get $get) => $get('unlimited_mem')) - ->label('Memory Limit')->inlineLabel() - ->suffix('MiB') - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - ]), + $word = (new RandomWordService())->word(); - Forms\Components\Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - Forms\Components\ToggleButtons::make('unlimited_disk') - ->label('Disk Space')->inlineLabel()->inline() - ->live() - ->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0)) - ->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0) - ->options([ - true => 'Unlimited', - false => 'Limited', - ]) - ->colors([ - true => 'primary', - false => 'warning', - ]) - ->columnSpan(2), - - Forms\Components\TextInput::make('disk') - ->dehydratedWhenHidden() - ->hidden(fn (Forms\Get $get) => $get('unlimited_disk')) - ->label('Disk Space Limit')->inlineLabel() - ->suffix('MiB') - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - ]), - - Forms\Components\Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - Forms\Components\ToggleButtons::make('unlimited_cpu') - ->label('CPU')->inlineLabel()->inline() - ->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0)) - ->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0) - ->live() - ->options([ - true => 'Unlimited', - false => 'Limited', - ]) - ->colors([ - true => 'primary', - false => 'warning', - ]) - ->columnSpan(2), - - Forms\Components\TextInput::make('cpu') - ->dehydratedWhenHidden() - ->hidden(fn (Forms\Get $get) => $get('unlimited_cpu')) - ->label('CPU Limit')->inlineLabel() - ->suffix('%') - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - ]), - - Forms\Components\Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - Forms\Components\ToggleButtons::make('swap_support') - ->live() - ->label('Enable Swap Memory')->inlineLabel()->inline() - ->columnSpan(2) - ->afterStateUpdated(function ($state, Forms\Set $set) { - $value = match ($state) { - 'unlimited' => -1, - 'disabled' => 0, - 'limited' => 128, - }; - - $set('swap', $value); - }) - ->formatStateUsing(function (Forms\Get $get) { - return match (true) { - $get('swap') > 0 => 'limited', - $get('swap') == 0 => 'disabled', - $get('swap') < 0 => 'unlimited', - }; - }) - ->options([ - 'unlimited' => 'Unlimited', - 'limited' => 'Limited', - 'disabled' => 'Disabled', - ]) - ->colors([ - 'unlimited' => 'primary', - 'limited' => 'warning', - 'disabled' => 'danger', - ]), - - Forms\Components\TextInput::make('swap') - ->dehydratedWhenHidden() - ->hidden(fn (Forms\Get $get) => match ($get('swap_support')) { - 'disabled', 'unlimited', true => true, - 'limited', false => false, - }) - ->label('Swap Memory')->inlineLabel() - ->suffix('MiB') - ->minValue(-1) - ->columnSpan(2) - ->required() - ->integer(), - ]), - - Forms\Components\Hidden::make('io') - ->helperText('The IO performance relative to other running containers') - ->label('Block IO Proportion'), - - Forms\Components\Grid::make() - ->columns(4) - ->columnSpanFull() - ->schema([ - Forms\Components\ToggleButtons::make('oom_killer') - ->label('OOM Killer')->inlineLabel()->inline() - ->columnSpan(2) - ->options([ - false => 'Disabled', - true => 'Enabled', - ]) - ->colors([ - false => 'success', - true => 'danger', - ]), - - Forms\Components\TextInput::make('oom_disabled_hidden') - ->hidden(), - ]), - ]), - - Forms\Components\Fieldset::make('Feature Limits') - ->inlineLabel() - ->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') + $set('name', $prefix . $word); + })) + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 3, + ]) ->required() - ->numeric(), - Forms\Components\TextInput::make('database_limit') - ->suffixIcon('tabler-database') - ->required() - ->numeric(), - Forms\Components\TextInput::make('backup_limit') - ->suffixIcon('tabler-copy-check') - ->required() - ->numeric(), - ]), - Forms\Components\Fieldset::make('Docker Settings') - ->columnSpan([ - 'default' => 2, - 'sm' => 4, - 'md' => 4, - 'lg' => 6, - ]) - ->columns([ - 'default' => 1, - 'sm' => 2, - 'md' => 3, - 'lg' => 3, - ]) - ->schema([ - Forms\Components\Select::make('select_image') - ->label('Image Name') - ->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state)) - ->options(function ($state, Forms\Get $get, Forms\Set $set) { - $egg = Egg::query()->find($get('egg_id')); - $images = $egg->docker_images ?? []; + ->maxLength(191), - $currentImage = $get('image'); - if (!$currentImage && $images) { - $defaultImage = collect($images)->first(); - $set('image', $defaultImage); - $set('select_image', $defaultImage); - } + Forms\Components\Select::make('owner_id') + ->prefixIcon('tabler-user') + ->label('Owner') + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 3, + ]) + ->relationship('user', 'username') + ->searchable() + ->preload() + ->required(), - return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image']; - }) - ->selectablePlaceholder(false) - ->columnSpan(1), - - Forms\Components\TextInput::make('image') - ->label('Image') - ->debounce(500) - ->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) { - $egg = Egg::query()->find($get('egg_id')); - $images = $egg->docker_images ?? []; - - if (in_array($state, $images)) { - $set('select_image', $state); - } else { - $set('select_image', 'ghcr.io/custom-image'); - } - }) - ->placeholder('Enter a custom Image') - ->columnSpan(1), - - Forms\Components\KeyValue::make('docker_labels') - ->label('Container Labels') - ->keyLabel('Label Name') - ->valueLabel('Label Description') + Forms\Components\Textarea::make('description') + ->label('Description') ->columnSpanFull(), + + Forms\Components\TextInput::make('uuid') + ->hintAction(CopyAction::make()) + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 3, + ]) + ->readOnly(), + Forms\Components\TextInput::make('uuid_short') + ->label('Short UUID') + ->hintAction(CopyAction::make()) + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 3, + ]) + ->readOnly(), + Forms\Components\TextInput::make('external_id') + ->label('External ID') + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 3, + ]) + ->maxLength(191), + Forms\Components\Select::make('node_id') + ->label('Node') + ->relationship('node', 'name') + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 3, + ]) + ->disabled(), + ]), + Tabs\Tab::make('Environment') + ->schema([ + Forms\Components\Fieldset::make('Resource Limits') + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ]) + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 3, + ]) + ->schema([ + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('unlimited_mem') + ->label('Memory')->inlineLabel()->inline() + ->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0)) + ->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0) + ->live() + ->options([ + true => 'Unlimited', + false => 'Limited', + ]) + ->colors([ + true => 'primary', + false => 'warning', + ]) + ->columnSpan(2), + + Forms\Components\TextInput::make('memory') + ->dehydratedWhenHidden() + ->hidden(fn (Forms\Get $get) => $get('unlimited_mem')) + ->label('Memory Limit')->inlineLabel() + ->suffix('MiB') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('unlimited_disk') + ->label('Disk Space')->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0)) + ->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0) + ->options([ + true => 'Unlimited', + false => 'Limited', + ]) + ->colors([ + true => 'primary', + false => 'warning', + ]) + ->columnSpan(2), + + Forms\Components\TextInput::make('disk') + ->dehydratedWhenHidden() + ->hidden(fn (Forms\Get $get) => $get('unlimited_disk')) + ->label('Disk Space Limit')->inlineLabel() + ->suffix('MiB') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('unlimited_cpu') + ->label('CPU')->inlineLabel()->inline() + ->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0)) + ->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0) + ->live() + ->options([ + true => 'Unlimited', + false => 'Limited', + ]) + ->colors([ + true => 'primary', + false => 'warning', + ]) + ->columnSpan(2), + + Forms\Components\TextInput::make('cpu') + ->dehydratedWhenHidden() + ->hidden(fn (Forms\Get $get) => $get('unlimited_cpu')) + ->label('CPU Limit')->inlineLabel() + ->suffix('%') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('swap_support') + ->live() + ->label('Enable Swap Memory')->inlineLabel()->inline() + ->columnSpan(2) + ->afterStateUpdated(function ($state, Forms\Set $set) { + $value = match ($state) { + 'unlimited' => -1, + 'disabled' => 0, + 'limited' => 128, + }; + + $set('swap', $value); + }) + ->formatStateUsing(function (Forms\Get $get) { + return match (true) { + $get('swap') > 0 => 'limited', + $get('swap') == 0 => 'disabled', + $get('swap') < 0 => 'unlimited', + }; + }) + ->options([ + 'unlimited' => 'Unlimited', + 'limited' => 'Limited', + 'disabled' => 'Disabled', + ]) + ->colors([ + 'unlimited' => 'primary', + 'limited' => 'warning', + 'disabled' => 'danger', + ]), + + Forms\Components\TextInput::make('swap') + ->dehydratedWhenHidden() + ->hidden(fn (Forms\Get $get) => match ($get('swap_support')) { + 'disabled', 'unlimited', true => true, + 'limited', false => false, + }) + ->label('Swap Memory')->inlineLabel() + ->suffix('MiB') + ->minValue(-1) + ->columnSpan(2) + ->required() + ->integer(), + ]), + + Forms\Components\Hidden::make('io') + ->helperText('The IO performance relative to other running containers') + ->label('Block IO Proportion'), + + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('oom_killer') + ->label('OOM Killer')->inlineLabel()->inline() + ->columnSpan(2) + ->options([ + false => 'Disabled', + true => 'Enabled', + ]) + ->colors([ + false => 'success', + true => 'danger', + ]), + + Forms\Components\TextInput::make('oom_disabled_hidden') + ->hidden(), + ]), + ]), + + Forms\Components\Fieldset::make('Feature Limits') + ->inlineLabel() + ->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(), + Forms\Components\TextInput::make('database_limit') + ->suffixIcon('tabler-database') + ->required() + ->numeric(), + Forms\Components\TextInput::make('backup_limit') + ->suffixIcon('tabler-copy-check') + ->required() + ->numeric(), + ]), + Forms\Components\Fieldset::make('Docker Settings') + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ]) + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 3, + ]) + ->schema([ + Forms\Components\Select::make('select_image') + ->label('Image Name') + ->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state)) + ->options(function ($state, Forms\Get $get, Forms\Set $set) { + $egg = Egg::query()->find($get('egg_id')); + $images = $egg->docker_images ?? []; + + $currentImage = $get('image'); + if (!$currentImage && $images) { + $defaultImage = collect($images)->first(); + $set('image', $defaultImage); + $set('select_image', $defaultImage); + } + + return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image']; + }) + ->selectablePlaceholder(false) + ->columnSpan(1), + + Forms\Components\TextInput::make('image') + ->label('Image') + ->debounce(500) + ->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) { + $egg = Egg::query()->find($get('egg_id')); + $images = $egg->docker_images ?? []; + + if (in_array($state, $images)) { + $set('select_image', $state); + } else { + $set('select_image', 'ghcr.io/custom-image'); + } + }) + ->placeholder('Enter a custom Image') + ->columnSpan(2), + + Forms\Components\KeyValue::make('docker_labels') + ->label('Container Labels') + ->keyLabel('Label Name') + ->valueLabel('Label Description') + ->columnSpanFull(), + ]), + ]), + Tabs\Tab::make('Egg') + ->columns([ + 'default' => 1, + 'sm' => 3, + 'md' => 3, + 'lg' => 5, + ]) + ->schema([ + Forms\Components\Select::make('egg_id') + ->disabledOn('edit') + ->prefixIcon('tabler-egg') + ->columnSpan([ + 'default' => 1, + 'sm' => 3, + 'md' => 3, + 'lg' => 5, + ]) + ->relationship('egg', 'name') + ->searchable() + ->preload() + ->required(), + + Forms\Components\ToggleButtons::make('skip_scripts') + ->label('Run Egg Install Script?')->inline() + ->options([ + false => 'Yes', + true => 'Skip', + ]) + ->colors([ + false => 'primary', + true => 'danger', + ]) + ->icons([ + false => 'tabler-code', + true => 'tabler-code-off', + ]) + ->required(), + + Forms\Components\Textarea::make('startup') + ->label('Startup Command') + ->required() + ->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\Textarea::make('defaultStartup') + ->hintAction(CopyAction::make()) + ->label('Default Startup Command') + ->disabled() + ->formatStateUsing(function ($state, Forms\Get $get, Forms\Set $set) { + $egg = Egg::query()->find($get('egg_id')); + + return $egg->startup; + }) + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ]), + + Forms\Components\Repeater::make('server_variables') + ->relationship('serverVariables') + ->grid() + ->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array { + foreach ($data as $key => $value) { + if (!isset($data['variable_value'])) { + $data['variable_value'] = ''; + } + } + + return $data; + }) + ->reorderable(false)->addable(false)->deletable(false) + ->schema(function () { + + $text = Forms\Components\TextInput::make('variable_value') + ->hidden($this->shouldHideComponent(...)) + ->maxLength(191) + ->rules([ + fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) { + $validator = Validator::make(['validatorkey' => $value], [ + 'validatorkey' => $serverVariable->variable->rules, + ]); + + if ($validator->fails()) { + $message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name); + + $fail($message); + } + }, + ]); + + $select = Forms\Components\Select::make('variable_value') + ->hidden($this->shouldHideComponent(...)) + ->options($this->getSelectOptionsFromRules(...)) + ->selectablePlaceholder(false); + + $components = [$text, $select]; + + /** @var Forms\Components\Component $component */ + foreach ($components as &$component) { + $component = $component + ->live(onBlur: true) + ->hintIcon('tabler-code') + ->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name) + ->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules) + ->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}') + ->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description); + } + + return $components; + }) + ->columnSpan(6), + ]), + Tabs\Tab::make('Mounts'), + Tabs\Tab::make('Databases'), + Tabs\Tab::make('Actions') + ->schema([ + Forms\Components\Fieldset::make('Server Actions') + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 2, + 'lg' => 6, + ]) + ->schema([ + Forms\Components\Grid::make() + ->columnSpan(3) + ->schema([ + Forms\Components\Actions::make([ + Forms\Components\Actions\Action::make('toggleInstall') + ->label('Toggle Status') + ->action(fn (ServersController $serversController, Server $server) => $serversController->toggleInstall($server)), + ])->fullWidth(), + Forms\Components\ToggleButtons::make('') + ->hint('If you need to change the install status from uninstalled to installed, or vice versa, you may do so with this button.'), + ]), + Forms\Components\Grid::make() + ->columnSpan(3) + ->schema([ + Forms\Components\Actions::make([ + Forms\Components\Actions\Action::make('toggleSuspend') + ->label('Suspend') + ->color('warning') + ->hidden(fn (Server $server) => $server->isSuspended()) + ->action(fn (SuspensionService $suspensionService, Server $server) => $suspensionService->toggle($server, 'suspend')), + Forms\Components\Actions\Action::make('toggleUnsuspend') + ->label('Unsuspend') + ->color('success') + ->hidden(fn (Server $server) => !$server->isSuspended()) + ->action(fn (SuspensionService $suspensionService, Server $server) => $suspensionService->toggle($server, 'unsuspend')), + ])->fullWidth(), + Forms\Components\ToggleButtons::make('') + ->hidden(fn (Server $server) => $server->isSuspended()) + ->hint('This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API.'), + Forms\Components\ToggleButtons::make('') + ->hidden(fn (Server $server) => !$server->isSuspended()) + ->hint('This will unsuspend the server and restore normal user access.'), + ]), + Forms\Components\Grid::make() + ->columnSpan(3) + ->schema([ + Forms\Components\Actions::make([ + Forms\Components\Actions\Action::make('transfer') + ->label('Transfer'), + ])->fullWidth(), + Forms\Components\ToggleButtons::make('') + ->hint('Transfer this server to another node connected to this panel. Warning! This feature has not been fully tested and may have bugs.'), + ]), + Forms\Components\Grid::make() + ->columnSpan(3) + ->schema([ + Forms\Components\Actions::make([ + Forms\Components\Actions\Action::make('reinstall') + ->label('Reinstall') + ->color('danger') + ->requiresConfirmation() + ->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)), + ])->fullWidth(), + Forms\Components\ToggleButtons::make('') + ->hint('This will reinstall the server with the assigned egg scripts. Danger! This could overwrite server data.'), + ]), + ]), ]), ]), ]); @@ -543,6 +655,10 @@ class EditServer extends EditRecord protected function mutateFormDataBeforeSave(array $data): array { + if (!isset($data['description'])) { + $data['description'] = ''; + } + unset($data['docker'], $data['status']); return $data; diff --git a/app/Filament/Resources/ServerResource/Pages/EditServerOrg.php b/app/Filament/Resources/ServerResource/Pages/EditServerOrg.php new file mode 100644 index 000000000..1ce585936 --- /dev/null +++ b/app/Filament/Resources/ServerResource/Pages/EditServerOrg.php @@ -0,0 +1,588 @@ +columns([ + 'default' => 2, + 'sm' => 2, + 'md' => 4, + 'lg' => 6, + ]) + ->schema([ + Forms\Components\ToggleButtons::make('docker') + ->label('Container Status')->inline()->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, + ]), + + Forms\Components\ToggleButtons::make('status') + ->label('Server State')->inline()->inlineLabel() + ->helperText('') + + ->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, + ]), + + 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)) + ->action(function (Forms\Set $set, Forms\Get $get) { + $egg = Egg::find($get('egg_id')); + $prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : ''; + + $word = (new RandomWordService())->word(); + + $set('name', $prefix . $word); + })) + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 2, + 'lg' => 3, + ]) + ->required() + ->maxLength(191), + + Forms\Components\Select::make('owner_id') + ->prefixIcon('tabler-user') + ->label('Owner') + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 2, + 'lg' => 3, + ]) + ->relationship('user', 'username') + ->searchable() + ->preload() + ->required(), + + Forms\Components\Textarea::make('description') + ->hidden() + ->required() + ->columnSpanFull(), + + Forms\Components\Select::make('egg_id') + ->disabledOn('edit') + ->prefixIcon('tabler-egg') + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 2, + 'lg' => 5, + ]) + ->relationship('egg', 'name') + ->searchable() + ->preload() + ->required(), + + Forms\Components\ToggleButtons::make('skip_scripts') + ->label('Run Egg Install Script?')->inline() + ->options([ + false => 'Yes', + true => 'Skip', + ]) + ->colors([ + false => 'primary', + true => 'danger', + ]) + ->icons([ + false => 'tabler-code', + true => 'tabler-code-off', + ]) + ->required(), + + 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('start_on_completion'), + + 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\Repeater::make('server_variables') + ->relationship('serverVariables') + ->grid() + ->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array { + foreach ($data as $key => $value) { + if (!isset($data['variable_value'])) { + $data['variable_value'] = ''; + } + } + + return $data; + }) + ->reorderable(false)->addable(false)->deletable(false) + ->schema(function () { + + $text = Forms\Components\TextInput::make('variable_value') + ->hidden($this->shouldHideComponent(...)) + ->maxLength(191) + ->rules([ + fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) { + $validator = Validator::make(['validatorkey' => $value], [ + 'validatorkey' => $serverVariable->variable->rules, + ]); + + if ($validator->fails()) { + $message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name); + + $fail($message); + } + }, + ]); + + $select = Forms\Components\Select::make('variable_value') + ->hidden($this->shouldHideComponent(...)) + ->options($this->getSelectOptionsFromRules(...)) + ->selectablePlaceholder(false); + + $components = [$text, $select]; + + /** @var Forms\Components\Component $component */ + foreach ($components as &$component) { + $component = $component + ->live(onBlur: true) + ->hintIcon('tabler-code') + ->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name) + ->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules) + ->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}') + ->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description); + } + + return $components; + }) + ->columnSpan(2), + ]), + + Forms\Components\Section::make('Environment Management') + ->collapsed() + ->icon('tabler-server-cog') + ->iconColor('primary') + ->columns([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 4, + ]) + ->columnSpanFull() + ->schema([ + Forms\Components\Fieldset::make('Resource Limits') + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ]) + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 3, + ]) + ->schema([ + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('unlimited_mem') + ->label('Memory')->inlineLabel()->inline() + ->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0)) + ->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0) + ->live() + ->options([ + true => 'Unlimited', + false => 'Limited', + ]) + ->colors([ + true => 'primary', + false => 'warning', + ]) + ->columnSpan(2), + + Forms\Components\TextInput::make('memory') + ->dehydratedWhenHidden() + ->hidden(fn (Forms\Get $get) => $get('unlimited_mem')) + ->label('Memory Limit')->inlineLabel() + ->suffix('MiB') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('unlimited_disk') + ->label('Disk Space')->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0)) + ->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0) + ->options([ + true => 'Unlimited', + false => 'Limited', + ]) + ->colors([ + true => 'primary', + false => 'warning', + ]) + ->columnSpan(2), + + Forms\Components\TextInput::make('disk') + ->dehydratedWhenHidden() + ->hidden(fn (Forms\Get $get) => $get('unlimited_disk')) + ->label('Disk Space Limit')->inlineLabel() + ->suffix('MiB') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('unlimited_cpu') + ->label('CPU')->inlineLabel()->inline() + ->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0)) + ->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0) + ->live() + ->options([ + true => 'Unlimited', + false => 'Limited', + ]) + ->colors([ + true => 'primary', + false => 'warning', + ]) + ->columnSpan(2), + + Forms\Components\TextInput::make('cpu') + ->dehydratedWhenHidden() + ->hidden(fn (Forms\Get $get) => $get('unlimited_cpu')) + ->label('CPU Limit')->inlineLabel() + ->suffix('%') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + ]), + + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('swap_support') + ->live() + ->label('Enable Swap Memory')->inlineLabel()->inline() + ->columnSpan(2) + ->afterStateUpdated(function ($state, Forms\Set $set) { + $value = match ($state) { + 'unlimited' => -1, + 'disabled' => 0, + 'limited' => 128, + }; + + $set('swap', $value); + }) + ->formatStateUsing(function (Forms\Get $get) { + return match (true) { + $get('swap') > 0 => 'limited', + $get('swap') == 0 => 'disabled', + $get('swap') < 0 => 'unlimited', + }; + }) + ->options([ + 'unlimited' => 'Unlimited', + 'limited' => 'Limited', + 'disabled' => 'Disabled', + ]) + ->colors([ + 'unlimited' => 'primary', + 'limited' => 'warning', + 'disabled' => 'danger', + ]), + + Forms\Components\TextInput::make('swap') + ->dehydratedWhenHidden() + ->hidden(fn (Forms\Get $get) => match ($get('swap_support')) { + 'disabled', 'unlimited', true => true, + 'limited', false => false, + }) + ->label('Swap Memory')->inlineLabel() + ->suffix('MiB') + ->minValue(-1) + ->columnSpan(2) + ->required() + ->integer(), + ]), + + Forms\Components\Hidden::make('io') + ->helperText('The IO performance relative to other running containers') + ->label('Block IO Proportion'), + + Forms\Components\Grid::make() + ->columns(4) + ->columnSpanFull() + ->schema([ + Forms\Components\ToggleButtons::make('oom_killer') + ->label('OOM Killer')->inlineLabel()->inline() + ->columnSpan(2) + ->options([ + false => 'Disabled', + true => 'Enabled', + ]) + ->colors([ + false => 'success', + true => 'danger', + ]), + + Forms\Components\TextInput::make('oom_disabled_hidden') + ->hidden(), + ]), + ]), + + Forms\Components\Fieldset::make('Feature Limits') + ->inlineLabel() + ->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(), + Forms\Components\TextInput::make('database_limit') + ->suffixIcon('tabler-database') + ->required() + ->numeric(), + Forms\Components\TextInput::make('backup_limit') + ->suffixIcon('tabler-copy-check') + ->required() + ->numeric(), + ]), + Forms\Components\Fieldset::make('Docker Settings') + ->columnSpan([ + 'default' => 2, + 'sm' => 4, + 'md' => 4, + 'lg' => 6, + ]) + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 3, + 'lg' => 3, + ]) + ->schema([ + Forms\Components\Select::make('select_image') + ->label('Image Name') + ->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state)) + ->options(function ($state, Forms\Get $get, Forms\Set $set) { + $egg = Egg::query()->find($get('egg_id')); + $images = $egg->docker_images ?? []; + + $currentImage = $get('image'); + if (!$currentImage && $images) { + $defaultImage = collect($images)->first(); + $set('image', $defaultImage); + $set('select_image', $defaultImage); + } + + return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image']; + }) + ->selectablePlaceholder(false) + ->columnSpan(1), + + Forms\Components\TextInput::make('image') + ->label('Image') + ->debounce(500) + ->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) { + $egg = Egg::query()->find($get('egg_id')); + $images = $egg->docker_images ?? []; + + if (in_array($state, $images)) { + $set('select_image', $state); + } else { + $set('select_image', 'ghcr.io/custom-image'); + } + }) + ->placeholder('Enter a custom Image') + ->columnSpan(1), + + Forms\Components\KeyValue::make('docker_labels') + ->label('Container Labels') + ->keyLabel('Label Name') + ->valueLabel('Label Description') + ->columnSpanFull(), + ]), + ]), + ]); + } + protected function getHeaderActions(): array + { + return [ + Actions\DeleteAction::make('Delete') + ->successRedirectUrl(route('filament.admin.resources.servers.index')) + ->color('danger') + ->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server)) + ->requiresConfirmation(), + Actions\Action::make('console') + ->label('Console') + ->icon('tabler-terminal') + ->url(fn (Server $server) => "/server/$server->uuid_short"), + $this->getSaveFormAction()->formId('form'), + ]; + + } + protected function getFormActions(): array + { + return []; + } + + protected function mutateFormDataBeforeSave(array $data): array + { + unset($data['docker'], $data['status']); + + return $data; + } + + public function getRelationManagers(): array + { + return [ + ServerResource\RelationManagers\AllocationsRelationManager::class, + ]; + } + + private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool + { + $containsRuleIn = str($get('rules'))->explode('|')->reduce( + fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true + ); + + if ($component instanceof Forms\Components\Select) { + return $containsRuleIn; + } + + if ($component instanceof Forms\Components\TextInput) { + return !$containsRuleIn; + } + + throw new \Exception('Component type not supported: ' . $component::class); + } + + private function getSelectOptionsFromRules(Forms\Get $get): array + { + $inRule = str($get('rules'))->explode('|')->reduce( + fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, '' + ); + + return str($inRule) + ->after('in:') + ->explode(',') + ->each(fn ($value) => str($value)->trim()) + ->mapWithKeys(fn ($value) => [$value => $value]) + ->all(); + } +} diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 5a2b8074f..d71a689cf 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Admin; use App\Enums\ServerState; +use Filament\Notifications\Notification; use Illuminate\Http\Request; use App\Models\User; use Illuminate\Http\Response; @@ -70,7 +71,7 @@ class ServersController extends Controller * @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\Model\DataValidationException */ - public function toggleInstall(Server $server): RedirectResponse + public function toggleInstall(Server $server) { if ($server->status === ServerState::InstallFailed) { throw new DisplayException(trans('admin/server.exceptions.marked_as_failed')); @@ -79,9 +80,13 @@ class ServersController extends Controller $server->status = $server->isInstalled() ? ServerState::Installing : null; $server->save(); - $this->alert->success(trans('admin/server.alerts.install_toggled'))->flash(); + Notification::make() + ->title('Success!') + ->body(trans('admin/server.alerts.install_toggled')) + ->success() + ->send(); - return redirect()->route('admin.servers.view.manage', $server->id); + return null; } /** @@ -90,12 +95,15 @@ class ServersController extends Controller * @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\Model\DataValidationException */ - public function reinstallServer(Server $server): RedirectResponse + public function reinstallServer(Server $server) { $this->reinstallService->handle($server); - $this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash(); - return redirect()->route('admin.servers.view.manage', $server->id); + Notification::make() + ->title('Success!') + ->body(trans('admin/server.alerts.server_reinstalled')) + ->success() + ->send(); } /** diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index 5e317fc1b..aef02b7b2 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -3,6 +3,7 @@ namespace App\Services\Servers; use App\Enums\ServerState; +use Filament\Notifications\Notification; use Webmozart\Assert\Assert; use App\Models\Server; use App\Repositories\Daemon\DaemonServerRepository; @@ -26,7 +27,7 @@ class SuspensionService * * @throws \Throwable */ - public function toggle(Server $server, string $action = self::ACTION_SUSPEND): void + public function toggle(Server $server, string $action = self::ACTION_SUSPEND) { Assert::oneOf($action, [self::ACTION_SUSPEND, self::ACTION_UNSUSPEND]); @@ -35,11 +36,12 @@ class SuspensionService // suspended in the database. Additionally, nothing needs to happen if the server // is not suspended, and we try to un-suspend the instance. if ($isSuspending === $server->isSuspended()) { - return; + return Notification::make()->danger()->title('Failed!')->body('Server is already suspended!')->send(); } // Check if the server is currently being transferred. if (!is_null($server->transfer)) { + Notification::make()->danger()->title('Failed!')->body('Server is currently being transferred.')->send(); throw new ConflictHttpException('Cannot toggle suspension status on a server that is currently being transferred.'); } From d461242f08252adcb2e810e94df55886e4890420 Mon Sep 17 00:00:00 2001 From: notCharles Date: Mon, 27 May 2024 21:51:24 -0400 Subject: [PATCH 2/5] Improve Logic on buttons If a server is suspended, disable transfer/toggle/reinstall as they will unsuspend the server due to the status change. Also properly updates server state and container status. --- .../ServerResource/Pages/EditServer.php | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index ef4ce86bf..2d2e7392d 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -17,6 +17,7 @@ use App\Repositories\Daemon\DaemonServerRepository; use App\Services\Servers\ServerDeletionService; use Filament\Forms\Components\Tabs; use Filament\Forms\Form; +use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; use Illuminate\Support\Facades\Validator; use Closure; @@ -29,6 +30,12 @@ class EditServer extends EditRecord public function form(Form $form): Form { return $form + ->columns([ + 'default' => 1, + 'sm' => 2, + 'md' => 2, + 'lg' => 4, + ]) ->schema([ Forms\Components\ToggleButtons::make('docker') ->label('Container Status')->inline()->inlineLabel() @@ -56,13 +63,12 @@ class EditServer extends EditRecord 'default' => 1, 'sm' => 2, 'md' => 2, - 'lg' => 3, + 'lg' => 2, ]), Forms\Components\ToggleButtons::make('status') ->label('Server State')->inline()->inlineLabel() ->helperText('') - ->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()] @@ -77,7 +83,7 @@ class EditServer extends EditRecord 'default' => 1, 'sm' => 2, 'md' => 2, - 'lg' => 3, + 'lg' => 2, ]), Tabs::make('Tabs') @@ -577,7 +583,12 @@ class EditServer extends EditRecord Forms\Components\Actions::make([ Forms\Components\Actions\Action::make('toggleInstall') ->label('Toggle Status') - ->action(fn (ServersController $serversController, Server $server) => $serversController->toggleInstall($server)), + ->disabled(fn (Server $server) => $server->isSuspended()) + ->action(function (ServersController $serversController, Server $server) { + $serversController->toggleInstall($server); + + return $this->refreshFormData(['status', 'docker']); + }), ])->fullWidth(), Forms\Components\ToggleButtons::make('') ->hint('If you need to change the install status from uninstalled to installed, or vice versa, you may do so with this button.'), @@ -590,12 +601,22 @@ class EditServer extends EditRecord ->label('Suspend') ->color('warning') ->hidden(fn (Server $server) => $server->isSuspended()) - ->action(fn (SuspensionService $suspensionService, Server $server) => $suspensionService->toggle($server, 'suspend')), + ->action(function (SuspensionService $suspensionService, Server $server) { + $suspensionService->toggle($server, 'suspend'); + Notification::make()->success()->title('Server Suspended!')->send(); + + return $this->refreshFormData(['status', 'docker']); + }), Forms\Components\Actions\Action::make('toggleUnsuspend') ->label('Unsuspend') ->color('success') ->hidden(fn (Server $server) => !$server->isSuspended()) - ->action(fn (SuspensionService $suspensionService, Server $server) => $suspensionService->toggle($server, 'unsuspend')), + ->action(function (SuspensionService $suspensionService, Server $server) { + $suspensionService->toggle($server, 'unsuspend'); + Notification::make()->success()->title('Server Unsuspended!')->send(); + + return $this->refreshFormData(['status', 'docker']); + }), ])->fullWidth(), Forms\Components\ToggleButtons::make('') ->hidden(fn (Server $server) => $server->isSuspended()) @@ -609,6 +630,7 @@ class EditServer extends EditRecord ->schema([ Forms\Components\Actions::make([ Forms\Components\Actions\Action::make('transfer') + ->disabled(fn (Server $server) => $server->isSuspended()) ->label('Transfer'), ])->fullWidth(), Forms\Components\ToggleButtons::make('') @@ -622,6 +644,7 @@ class EditServer extends EditRecord ->label('Reinstall') ->color('danger') ->requiresConfirmation() + ->disabled(fn (Server $server) => $server->isSuspended()) ->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)), ])->fullWidth(), Forms\Components\ToggleButtons::make('') From dd223b47c07e0bf65066c066c2ba40ac2e206ac4 Mon Sep 17 00:00:00 2001 From: notCharles Date: Wed, 29 May 2024 18:27:54 -0400 Subject: [PATCH 3/5] WIP Server Transfer --- .../ServerResource/Pages/EditServer.php | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index 2d2e7392d..5595ea2d1 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -6,6 +6,7 @@ use App\Filament\Resources\ServerResource; use App\Http\Controllers\Admin\ServersController; use App\Services\Servers\RandomWordService; use App\Services\Servers\SuspensionService; +use App\Services\Servers\TransferServerService; use Filament\Actions; use Filament\Forms; use App\Enums\ContainerStatus; @@ -582,7 +583,7 @@ class EditServer extends EditRecord ->schema([ Forms\Components\Actions::make([ Forms\Components\Actions\Action::make('toggleInstall') - ->label('Toggle Status') + ->label('Toggle Install Status') ->disabled(fn (Server $server) => $server->isSuspended()) ->action(function (ServersController $serversController, Server $server) { $serversController->toggleInstall($server); @@ -630,8 +631,31 @@ class EditServer extends EditRecord ->schema([ Forms\Components\Actions::make([ Forms\Components\Actions\Action::make('transfer') - ->disabled(fn (Server $server) => $server->isSuspended()) - ->label('Transfer'), + ->label('Transfer') + ->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, $data)) + ->form([ + Forms\Components\Select::make('newNode') + ->label('New Node') + ->required() + ->options([ + true => 'on', + false => 'off', + ]), + Forms\Components\Select::make('newMainAllocation') + ->label('New Main Allocation') + ->required() + ->options([ + true => 'on', + false => 'off', + ]), + Forms\Components\Select::make('newAdditionalAllocation') + ->label('New Additional Allocations') + ->options([ + true => 'on', + false => 'off', + ]), + ]) + ->modalHeading('Transfer'), ])->fullWidth(), Forms\Components\ToggleButtons::make('') ->hint('Transfer this server to another node connected to this panel. Warning! This feature has not been fully tested and may have bugs.'), @@ -644,17 +668,32 @@ class EditServer extends EditRecord ->label('Reinstall') ->color('danger') ->requiresConfirmation() + ->modalHeading('Are you sure you want to reinstall this server?') + ->modalDescription('!! This can result in unrecoverable data loss !!') ->disabled(fn (Server $server) => $server->isSuspended()) ->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)), ])->fullWidth(), Forms\Components\ToggleButtons::make('') - ->hint('This will reinstall the server with the assigned egg scripts. Danger! This could overwrite server data.'), + ->hint('This will reinstall the server with the assigned egg install script.'), ]), ]), ]), ]), ]); } + + protected function transferServer(Form $form): Form + { + return $form + ->columns(2) + ->schema([ + Forms\Components\Select::make('toNode') + ->label('New Node'), + Forms\Components\TextInput::make('newAllocation') + ->label('Allocation'), + ]); + + } protected function getHeaderActions(): array { return [ From a6d07ede5a6ecda203d6966ffb8af2864b2504d2 Mon Sep 17 00:00:00 2001 From: notCharles Date: Wed, 29 May 2024 19:18:09 -0400 Subject: [PATCH 4/5] Soon-TM --- .../ServerResource/Pages/EditServer.php | 17 +++++++++++++---- app/Services/Servers/TransferServerService.php | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index 5595ea2d1..29bc00c89 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -566,8 +566,16 @@ class EditServer extends EditRecord }) ->columnSpan(6), ]), - Tabs\Tab::make('Mounts'), - Tabs\Tab::make('Databases'), + Tabs\Tab::make('Mounts') + ->schema([ + Forms\Components\Placeholder::make('soon') + ->label('Soon™'), + ]), + Tabs\Tab::make('Databases') + ->schema([ + Forms\Components\Placeholder::make('soon') + ->label('Soon™'), + ]), Tabs\Tab::make('Actions') ->schema([ Forms\Components\Fieldset::make('Server Actions') @@ -631,9 +639,10 @@ class EditServer extends EditRecord ->schema([ Forms\Components\Actions::make([ Forms\Components\Actions\Action::make('transfer') - ->label('Transfer') + ->label('Transfer Soon™') ->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, $data)) - ->form([ + ->disabled() //TODO! + ->form([ //TODO! Forms\Components\Select::make('newNode') ->label('New Node') ->required() diff --git a/app/Services/Servers/TransferServerService.php b/app/Services/Servers/TransferServerService.php index 2cc34db70..18f05dacf 100644 --- a/app/Services/Servers/TransferServerService.php +++ b/app/Services/Servers/TransferServerService.php @@ -53,7 +53,7 @@ class TransferServerService { $node_id = $data['node_id']; $allocation_id = intval($data['allocation_id']); - $additional_allocations = array_map('intval', $data['allocation_additional'] ?? []); + $additional_allocations = array_map(intval(...), $data['allocation_additional'] ?? []); // Check if the node is viable for the transfer. $node = Node::query() From 58d1fd39171f8a0e275a33f1404fb9155762315b Mon Sep 17 00:00:00 2001 From: notCharles Date: Sun, 2 Jun 2024 15:05:45 -0400 Subject: [PATCH 5/5] Add Mounts + Icons --- .../Resources/ServerResource/Pages/EditServer.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index 29bc00c89..932610d59 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -98,6 +98,7 @@ class EditServer extends EditRecord ]) ->tabs([ Tabs\Tab::make('Information') + ->icon('tabler-info-circle') ->schema([ Forms\Components\TextInput::make('name') ->prefixIcon('tabler-server') @@ -179,6 +180,7 @@ class EditServer extends EditRecord ->disabled(), ]), Tabs\Tab::make('Environment') + ->icon('tabler-brand-docker') ->schema([ Forms\Components\Fieldset::make('Resource Limits') ->columnSpan([ @@ -443,6 +445,7 @@ class EditServer extends EditRecord ]), ]), Tabs\Tab::make('Egg') + ->icon('tabler-egg') ->columns([ 'default' => 1, 'sm' => 3, @@ -567,16 +570,23 @@ class EditServer extends EditRecord ->columnSpan(6), ]), Tabs\Tab::make('Mounts') + ->icon('tabler-layers-linked') ->schema([ - Forms\Components\Placeholder::make('soon') - ->label('Soon™'), + Forms\Components\CheckboxList::make('mounts') + ->relationship('mounts') + ->options(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name])) + ->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"])) + ->label('Mounts') + ->columnSpanFull(), ]), Tabs\Tab::make('Databases') + ->icon('tabler-database') ->schema([ Forms\Components\Placeholder::make('soon') ->label('Soon™'), ]), Tabs\Tab::make('Actions') + ->icon('tabler-settings') ->schema([ Forms\Components\Fieldset::make('Server Actions') ->columns([