From ceb365b95c42cde0a392561c4de0c55f95bfdafa Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 23 Apr 2024 06:28:23 -0400 Subject: [PATCH] Split create/edit server pages --- .../ServerResource/Pages/CreateServer.php | 598 ++++++++++++++++++ 1 file changed, 598 insertions(+) diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index af95ddf20..f27767349 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -2,16 +2,614 @@ namespace App\Filament\Resources\ServerResource\Pages; +use App\Enums\ContainerStatus; +use App\Enums\ServerState; use App\Filament\Resources\ServerResource; +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\ServerCreationService; +use Filament\Forms\Form; use Filament\Resources\Pages\CreateRecord; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Validator; +use Closure; +use Filament\Forms; class CreateServer extends CreateRecord { protected static string $resource = ServerResource::class; protected static bool $canCreateAnother = false; + 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() + ->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->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(2) + ->reorderable(false) + ->addable(false) + ->deletable(false) + ->default([]) + ->hidden(fn ($state) => empty($state)) + ->schema([ + Forms\Components\TextInput::make('variable_value') + ->rules([ + fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) { + $validator = Validator::make(['validatorkey' => $value], [ + 'validatorkey' => $get('rules'), + ]); + + if ($validator->fails()) { + $message = str($validator->errors()->first())->replace('validatorkey', $get('name')); + + $fail($message); + } + }, + ]) + ->label(fn (Forms\Get $get) => $get('name')) + //->hint('Rule') + ->hintIcon('tabler-code') + ->hintIconTooltip(fn (Forms\Get $get) => $get('rules')) + ->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}') + ->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('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 handleRecordCreation(array $data): Model { $data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();