components([ Tabs::make()->tabs([ Tab::make('configuration') ->label(trans('admin/egg.tabs.configuration')) ->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6]) ->icon('tabler-egg') ->schema([ Grid::make(2) ->columnSpan(1) ->schema([ Image::make('', '') ->hidden(fn ($record) => !$record->image) ->url(fn ($record) => $record->image) ->alt('') ->alignJustify() ->imageSize(150) ->columnSpanFull(), Flex::make([ Action::make('uploadImage') ->iconButton() ->iconSize(IconSize::Large) ->icon('tabler-photo-up') ->modal() ->modalHeading('') ->modalSubmitActionLabel(trans('admin/egg.import.import_image')) ->schema([ Tabs::make() ->contained(false) ->tabs([ Tab::make(trans('admin/egg.import.url')) ->schema([ Hidden::make('base64Image'), TextInput::make('image_url') ->label(trans('admin/egg.import.image_url')) ->reactive() ->autocomplete(false) ->debounce(500) ->afterStateUpdated(function ($state, Set $set) { if (!$state) { $set('image_url_error', null); return; } try { if (!filter_var($state, FILTER_VALIDATE_URL)) { throw new \Exception(trans('admin/egg.import.invalid_url')); } $allowedExtensions = [ 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'webp' => 'image/webp', 'svg' => 'image/svg+xml', ]; $extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION)); if (!array_key_exists($extension, $allowedExtensions)) { throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', $allowedExtensions)])); } $host = parse_url($state, PHP_URL_HOST); $ip = gethostbyname($host); if ( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false ) { throw new \Exception(trans('admin/egg.import.no_local_ip')); } $context = stream_context_create([ 'http' => ['timeout' => 3], 'https' => [ 'timeout' => 3, 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $imageContent = @file_get_contents($state, false, $context, 0, 1048576); // 1024KB if (!$imageContent) { throw new \Exception(trans('admin/egg.import.image_error')); } if (strlen($imageContent) >= 1048576) { throw new \Exception(trans('admin/egg.import.image_too_large')); } $mimeType = $allowedExtensions[$extension]; $base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent); $set('base64Image', $base64); $set('image_url_error', null); } catch (\Exception $e) { $set('image_url_error', $e->getMessage()); $set('base64Image', null); } }), TextEntry::make('image_url_error') ->hiddenLabel() ->visible(fn ($get) => $get('image_url_error') !== null) ->afterStateHydrated(fn ($set, $get) => $get('image_url_error')), Image::make(fn (Get $get) => $get('image_url'), '') ->imageSize(150) ->visible(fn ($get) => $get('image_url') && !$get('image_url_error')) ->alignCenter(), ]), Tab::make(trans('admin/egg.import.file')) ->schema([ FileUpload::make('image') ->hiddenLabel() ->previewable() ->openable(false) ->downloadable(false) ->maxSize(1024) ->maxFiles(1) ->columnSpanFull() ->alignCenter() ->imageEditor() ->saveUploadedFileUsing(function ($file, Set $set) { $base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath())); $set('base64Image', $base64); return $base64; }), ]), ]), ]) ->action(function (array $data, $record): void { $base64 = $data['base64Image'] ?? null; if (empty($base64) && !empty($data['image'])) { $base64 = $data['image']; } if (!empty($base64)) { $record->update([ 'image' => $base64, ]); Notification::make() ->title(trans('admin/egg.import.image_updated')) ->success() ->send(); $record->refresh(); } else { Notification::make() ->title(trans('admin/egg.import.no_image')) ->warning() ->send(); } }), Action::make('deleteImage') ->visible(fn ($record) => $record->image) ->label('') ->icon('tabler-trash') ->iconButton() ->iconSize(IconSize::Large) ->color('danger') ->action(function ($record) { $record->update([ 'image' => null, ]); Notification::make() ->title(trans('admin/egg.import.image_deleted')) ->success() ->send(); $record->refresh(); }), ]), ]), TextInput::make('name') ->label(trans('admin/egg.name')) ->required() ->maxLength(255) ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2]) ->helperText(trans('admin/egg.name_help')), Textarea::make('description') ->label(trans('admin/egg.description')) ->rows(3) ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3]) ->helperText(trans('admin/egg.description_help')), TextInput::make('id') ->label(trans('admin/egg.egg_id')) ->columnSpan(1) ->disabled(), TextInput::make('uuid') ->label(trans('admin/egg.egg_uuid')) ->disabled() ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]) ->helperText(trans('admin/egg.uuid_help')), TextInput::make('author') ->label(trans('admin/egg.author')) ->required() ->maxLength(255) ->email() ->disabled() ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]) ->helperText(trans('admin/egg.author_help_edit')), Toggle::make('force_outgoing_ip') ->inline(false) ->label(trans('admin/egg.force_ip')) ->columnSpan(1) ->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')), KeyValue::make('startup_commands') ->label(trans('admin/egg.startup_commands')) ->live() ->columnSpanFull() ->required() ->addActionLabel(trans('admin/egg.add_startup')) ->keyLabel(trans('admin/egg.startup_name')) ->valueLabel(trans('admin/egg.startup_command')) ->helperText(trans('admin/egg.startup_help')), TagsInput::make('file_denylist') ->label(trans('admin/egg.file_denylist')) ->placeholder('denied-file.txt') ->helperText(trans('admin/egg.file_denylist_help')) ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), TextInput::make('update_url') ->label(trans('admin/egg.update_url')) ->url() ->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help')) ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), TagsInput::make('features') ->label(trans('admin/egg.features')) ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), Hidden::make('script_is_privileged') ->helperText('The docker images available to servers using this egg.'), TagsInput::make('tags') ->label(trans('admin/egg.tags')) ->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]), KeyValue::make('docker_images') ->label(trans('admin/egg.docker_images')) ->live() ->columnSpanFull() ->required() ->addActionLabel(trans('admin/egg.add_image')) ->keyLabel(trans('admin/egg.docker_name')) ->valueLabel(trans('admin/egg.docker_uri')) ->helperText(trans('admin/egg.docker_help')), ]), Tab::make('process_management') ->label(trans('admin/egg.tabs.process_management')) ->columns() ->icon('tabler-server-cog') ->schema([ CopyFrom::make('copy_process_from') ->process(), TextInput::make('config_stop') ->label(trans('admin/egg.stop_command')) ->maxLength(255) ->helperText(trans('admin/egg.stop_command_help')), Textarea::make('config_startup')->rows(10)->json() ->label(trans('admin/egg.start_config')) ->helperText(trans('admin/egg.start_config_help')), Textarea::make('config_files')->rows(10)->json() ->label(trans('admin/egg.config_files')) ->helperText(trans('admin/egg.config_files_help')), Textarea::make('config_logs')->rows(10)->json() ->label(trans('admin/egg.log_config')) ->helperText(trans('admin/egg.log_config_help')), ]), Tab::make('egg_variables') ->label(trans('admin/egg.tabs.egg_variables')) ->columnSpanFull() ->icon('tabler-variable') ->schema([ Repeater::make('variables') ->hiddenLabel() ->grid() ->relationship('variables') ->reorderable() ->collapsible()->collapsed() ->orderColumn() ->addActionLabel(trans('admin/egg.add_new_variable')) ->itemLabel(fn (array $state) => $state['name']) ->mutateRelationshipDataBeforeCreateUsing(function (array $data): array { $data['default_value'] ??= ''; $data['description'] ??= ''; $data['rules'] ??= []; $data['user_viewable'] ??= ''; $data['user_editable'] ??= ''; return $data; }) ->mutateRelationshipDataBeforeSaveUsing(function (array $data): array { $data['default_value'] ??= ''; $data['description'] ??= ''; $data['rules'] ??= []; $data['user_viewable'] ??= ''; $data['user_editable'] ??= ''; return $data; }) ->schema([ TextInput::make('name') ->label(trans('admin/egg.name')) ->live() ->debounce(750) ->maxLength(255) ->columnSpanFull() ->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())) ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id'))) ->validationMessages([ 'unique' => trans('admin/egg.error_unique'), ]) ->required(), Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(), TextInput::make('env_variable') ->label(trans('admin/egg.environment_variable')) ->maxLength(255) ->prefix('{{') ->suffix('}}') ->hintIcon('tabler-code', fn ($state) => "{{{$state}}}") ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id'))) ->rules(EggVariable::getRulesForField('env_variable')) ->validationMessages([ 'unique' => trans('admin/egg.error_unique'), 'required' => trans('admin/egg.error_required'), '*' => trans('admin/egg.error_reserved'), ]) ->required(), TextInput::make('default_value')->label(trans('admin/egg.default_value')), Fieldset::make(trans('admin/egg.user_permissions')) ->schema([ Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')), Checkbox::make('user_editable')->label(trans('admin/egg.editable')), ]), TagsInput::make('rules') ->label(trans('admin/egg.rules')) ->columnSpanFull() ->reorderable() ->suggestions([ 'required', 'nullable', 'string', 'integer', 'numeric', 'boolean', 'alpha', 'alpha_dash', 'alpha_num', 'url', 'email', 'regex:', 'min:', 'max:', 'between:', 'between:1024,65535', 'in:', 'in:true,false', ]), ]), ]), Tab::make('install_script') ->label(trans('admin/egg.tabs.install_script')) ->columns(3) ->icon('tabler-file-download') ->schema([ CopyFrom::make('copy_script_from') ->script(), TextInput::make('script_container') ->label(trans('admin/egg.script_container')) ->required() ->maxLength(255) ->placeholder('ghcr.io/pelican-eggs/installers:debian'), Select::make('script_entry') ->label(trans('admin/egg.script_entry')) ->selectablePlaceholder(false) ->options([ 'bash' => 'bash', 'ash' => 'ash', '/bin/bash' => '/bin/bash', ]) ->required(), CodeEditor::make('script_install') ->hiddenLabel() ->columnSpanFull(), ]), ])->columnSpanFull()->persistTabInQueryString(), ]); } /** @return array */ protected function getDefaultHeaderActions(): array { return [ DeleteAction::make() ->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0) ->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')), ExportEggAction::make(), ImportEggAction::make() ->multiple(false), $this->getSaveFormAction()->formId('form'), ]; } public function refreshForm(): void { $this->fillForm(); } protected function getFormActions(): array { return []; } }