From 65bb99e2b017bcd09e8c655a56435fe3ae601aa1 Mon Sep 17 00:00:00 2001 From: Charles Date: Fri, 21 Nov 2025 16:48:20 -0500 Subject: [PATCH] Add server icons (#1906) Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com> --- .../Resources/Servers/Pages/EditServer.php | 331 ++++++++++++++---- .../Resources/Servers/Pages/ListServers.php | 5 + app/Filament/Server/Pages/Settings.php | 238 +++++++++++-- app/Livewire/ServerEntry.php | 71 +--- app/Models/Server.php | 13 +- .../2025_11_16_000000_add_icon_to_servers.php | 28 ++ lang/en/server/setting.php | 6 + .../server-entry-placeholder.blade.php | 69 ++++ .../views/livewire/server-entry.blade.php | 16 +- 9 files changed, 599 insertions(+), 178 deletions(-) create mode 100644 database/migrations/2025_11_16_000000_add_icon_to_servers.php create mode 100644 resources/views/livewire/server-entry-placeholder.blade.php diff --git a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php index 59f1cee88..06e2679bc 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php @@ -29,6 +29,7 @@ use Exception; use Filament\Actions\Action; use Filament\Actions\ActionGroup; use Filament\Forms\Components\CodeEditor; +use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Hidden; use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\Repeater; @@ -38,12 +39,14 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Forms\Components\ToggleButtons; +use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Image; use Filament\Schemas\Components\StateCasts\BooleanStateCast; use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs\Tab; @@ -51,6 +54,7 @@ use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Schema; use Filament\Support\Enums\Alignment; +use Filament\Support\Enums\IconSize; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Arr; @@ -94,90 +98,265 @@ class EditServer extends EditRecord ->label(trans('admin/server.tabs.information')) ->icon('tabler-info-circle') ->schema([ - TextInput::make('name') - ->prefixIcon('tabler-server') - ->label(trans('admin/server.name')) - ->suffixAction(Action::make('random') - ->icon('tabler-dice-' . random_int(1, 6)) - ->action(function (Set $set, 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' => 1, - 'md' => 2, - 'lg' => 3, - ]) - ->required() - ->maxLength(255), - Select::make('owner_id') - ->prefixIcon('tabler-user') - ->label(trans('admin/server.owner')) - ->columnSpan([ - 'default' => 2, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]) - ->relationship('user', 'username') - ->searchable(['username', 'email']) - ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)") - ->preload() - ->required(), - ToggleButtons::make('condition') - ->label(trans('admin/server.server_status')) - ->formatStateUsing(fn (Server $server) => $server->condition) - ->options(fn ($state) => [$state->value => $state->getLabel()]) - ->colors(fn ($state) => [$state->value => $state->getColor()]) - ->icons(fn ($state) => [$state->value => $state->getIcon()]) - ->stateCast(new ServerConditionStateCast()) - ->columnSpan([ - 'default' => 2, - 'sm' => 1, - 'md' => 1, - 'lg' => 1, - ]) - ->hintAction( - Action::make('view_install_log') - ->label(trans('admin/server.view_install_log')) - //->visible(fn (Server $server) => $server->isFailedInstall()) - ->modalHeading('') - ->modalSubmitAction(false) - ->modalFooterActionsAlignment(Alignment::Right) - ->modalCancelActionLabel(trans('filament::components/modal.actions.close.label')) + Grid::make() + ->columns(2) + ->columnStart(1) + ->schema([ + Image::make('', 'icon') + ->hidden(fn ($record) => !$record->icon && !$record->egg->image) + ->url(fn ($record) => $record->icon ?: $record->egg->image) + ->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip')) + ->columnSpan(2) + ->alignJustify(), + Action::make('uploadIcon') + ->iconButton()->iconSize(IconSize::Large) + ->icon('tabler-photo-up') + ->modal() + ->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload')) ->schema([ - CodeEditor::make('logs') - ->hiddenLabel() - ->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) { - try { - $logs = $serverRepository->setServer($server)->getInstallLogs(); + Tabs::make()->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 mb_convert_encoding($logs, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'Windows-1252', 'ASCII']); - } catch (ConnectionException) { - Notification::make() - ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) - ->body(trans('admin/server.notifications.log_failed')) - ->color('warning') - ->warning() - ->send(); - } catch (Exception) { - return ''; - } + return; + } - return ''; - }), + try { + if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) { + throw new \Exception(trans('admin/egg.import.invalid_url')); + } + + 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(', ', array_keys($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, 262144); //256KB + + if (!$imageContent) { + throw new \Exception(trans('admin/egg.import.image_error')); + } + + $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) => $get('image_url_error') !== null) + ->afterStateHydrated(fn (Get $get) => $get('image_url_error')), + Image::make(fn (Get $get) => $get('image_url'), '') + ->imageSize(150) + ->visible(fn (Get $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(256) + ->maxFiles(1) + ->columnSpanFull() + ->alignCenter() + ->imageEditor() + ->image() + ->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([ + 'icon' => $base64, + ]); + + Notification::make() + ->title(trans('server/setting.server_info.icon.updated')) + ->success() + ->send(); + + $record->refresh(); + } else { + Notification::make() + ->title(trans('admin/egg.import.no_image')) + ->warning() + ->send(); + } + }), + Action::make('deleteIcon') + ->visible(fn ($record) => $record->icon) + ->label('') + ->icon('tabler-trash') + ->iconButton()->iconSize(IconSize::Large) + ->color('danger') + ->action(function ($record) { + $record->update([ + 'icon' => null, + ]); + + Notification::make() + ->title(trans('server/setting.server_info.icon.deleted')) + ->success() + ->send(); + + $record->refresh(); + }), + ]), + Grid::make() + ->columns(3) + ->columnStart(2) + ->columnSpan([ + 'default' => 2, + 'sm' => 2, + 'md' => 3, + 'lg' => 5, + ]) + ->schema([ + TextInput::make('name') + ->prefixIcon('tabler-server') + ->label(trans('admin/server.name')) + ->suffixAction(Action::make('random') + ->icon('tabler-dice-' . random_int(1, 6)) + ->action(function (Set $set, 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' => 1, + 'md' => 2, + 'lg' => 3, + ]) + ->required() + ->maxLength(255), + Select::make('owner_id') + ->prefixIcon('tabler-user') + ->label(trans('admin/server.owner')) + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]) + ->relationship('user', 'username') + ->searchable(['username', 'email']) + ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)") + ->preload() + ->required(), + ToggleButtons::make('condition') + ->label(trans('admin/server.server_status')) + ->formatStateUsing(fn (Server $server) => $server->condition) + ->options(fn ($state) => [$state->value => $state->getLabel()]) + ->colors(fn ($state) => [$state->value => $state->getColor()]) + ->icons(fn ($state) => [$state->value => $state->getIcon()]) + ->stateCast(new ServerConditionStateCast()) + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 1, + 'lg' => 1, + ]) + ->hintAction( + Action::make('view_install_log') + ->label(trans('admin/server.view_install_log')) + //->visible(fn (Server $server) => $server->isFailedInstall()) + ->modalHeading('') + ->modalSubmitAction(false) + ->modalFooterActionsAlignment(Alignment::Right) + ->modalCancelActionLabel(trans('filament::components/modal.actions.close.label')) + ->schema([ + CodeEditor::make('logs') + ->hiddenLabel() + ->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) { + try { + $logs = $serverRepository->setServer($server)->getInstallLogs(); + + return mb_convert_encoding($logs, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'Windows-1252', 'ASCII']); + } catch (ConnectionException) { + Notification::make() + ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name])) + ->body(trans('admin/server.notifications.log_failed')) + ->color('warning') + ->warning() + ->send(); + } catch (Exception) { + return ''; + } + + return ''; + }), + ]) + ), + ]), Textarea::make('description') ->label(trans('admin/server.description')) ->columnSpanFull(), - TextInput::make('uuid') ->label(trans('admin/server.uuid')) ->copyable() diff --git a/app/Filament/App/Resources/Servers/Pages/ListServers.php b/app/Filament/App/Resources/Servers/Pages/ListServers.php index cf59ccf8e..f8a7ca8f3 100644 --- a/app/Filament/App/Resources/Servers/Pages/ListServers.php +++ b/app/Filament/App/Resources/Servers/Pages/ListServers.php @@ -20,6 +20,7 @@ use Filament\Schemas\Components\Tabs\Tab; use Filament\Support\Enums\Alignment; use Filament\Support\Enums\IconSize; use Filament\Tables\Columns\Column; +use Filament\Tables\Columns\ImageColumn; use Filament\Tables\Columns\Layout\Stack; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; @@ -62,6 +63,10 @@ class ListServers extends ListRecords protected function tableColumns(): array { return [ + ImageColumn::make('icon') + ->label('') + ->imageSize(46) + ->state(fn (Server $server) => $server->icon ?: $server->egg->image), TextColumn::make('condition') ->label(trans('server/dashboard.status')) ->badge() diff --git a/app/Filament/Server/Pages/Settings.php b/app/Filament/Server/Pages/Settings.php index 811aeb151..6732a07b1 100644 --- a/app/Filament/Server/Pages/Settings.php +++ b/app/Filament/Server/Pages/Settings.php @@ -8,14 +8,23 @@ use App\Models\Server; use App\Services\Servers\ReinstallServerService; use Exception; use Filament\Actions\Action; +use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; use Filament\Schemas\Components\Fieldset; +use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Image; use Filament\Schemas\Components\Section; +use Filament\Schemas\Components\Tabs; +use Filament\Schemas\Components\Tabs\Tab; +use Filament\Schemas\Components\Utilities\Get; +use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Schema; use Filament\Support\Enums\Alignment; +use Filament\Support\Enums\IconSize; class Settings extends ServerFormPage { @@ -29,51 +38,208 @@ class Settings extends ServerFormPage public function form(Schema $schema): Schema { return parent::form($schema) - ->columns(4) ->components([ Section::make(trans('server/setting.server_info.title')) ->columnSpanFull() ->columns([ 'default' => 1, - 'sm' => 2, + 'sm' => 1, 'md' => 4, 'lg' => 6, ]) ->schema([ Fieldset::make() ->label(trans('server/setting.server_info.information')) - ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 6, - ]) + ->columnSpanFull() ->schema([ - TextInput::make('name') - ->label(trans('server/setting.server_info.name')) - ->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_RENAME, $server)) - ->required() - ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 6, - ]) - ->live(onBlur: true) - ->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)), - Textarea::make('description') - ->label(trans('server/setting.server_info.description')) - ->hidden(!config('panel.editable_server_descriptions')) - ->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_DESCRIPTION, $server)) - ->columnSpan([ - 'default' => 1, - 'sm' => 2, - 'md' => 2, - 'lg' => 6, - ]) - ->autosize() - ->live(onBlur: true) - ->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)), + Grid::make() + ->columns(2) + ->columnSpan(5) + ->schema([ + TextInput::make('name') + ->columnStart(1) + ->columnSpanFull() + ->label(trans('server/setting.server_info.name')) + ->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_RENAME, $server)) + ->required() + ->live(onBlur: true) + ->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)), + Textarea::make('description') + ->columnStart(1) + ->columnSpanFull() + ->label(trans('server/setting.server_info.description')) + ->hidden(!config('panel.editable_server_descriptions')) + ->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_DESCRIPTION, $server)) + ->autosize() + ->live(onBlur: true) + ->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)), + ]), + Grid::make() + ->columns(2) + ->columnStart(6) + ->schema([ + Image::make('', 'icon') + ->hidden(fn ($record) => !$record->icon && !$record->egg->image) + ->url(fn ($record) => $record->icon ?: $record->egg->image) + ->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip')) + ->columnSpan(2) + ->alignJustify(), + Action::make('uploadIcon') + ->iconButton()->iconSize(IconSize::Large) + ->icon('tabler-photo-up') + ->modal() + ->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload')) + ->schema([ + Tabs::make()->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 (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) { + throw new \Exception(trans('admin/egg.import.invalid_url')); + } + + 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(', ', array_keys($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, 262144); //256KB + + if (!$imageContent) { + throw new \Exception(trans('admin/egg.import.image_error')); + } + + $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) => $get('image_url_error') !== null) + ->afterStateHydrated(fn (Get $get) => $get('image_url_error')), + Image::make(fn (Get $get) => $get('image_url'), '') + ->imageSize(150) + ->visible(fn (Get $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(256) + ->maxFiles(1) + ->columnSpanFull() + ->alignCenter() + ->imageEditor() + ->image() + ->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([ + 'icon' => $base64, + ]); + + Notification::make() + ->title(trans('server/setting.server_info.icon.updated')) + ->success() + ->send(); + + $record->refresh(); + } else { + Notification::make() + ->title(trans('admin/egg.import.no_image')) + ->warning() + ->send(); + } + }), + Action::make('deleteIcon') + ->visible(fn ($record) => $record->icon) + ->label('') + ->icon('tabler-trash') + ->iconButton()->iconSize(IconSize::Large) + ->color('danger') + ->action(function ($record) { + $record->update([ + 'icon' => null, + ]); + + Notification::make() + ->title(trans('server/setting.server_info.icon.deleted')) + ->success() + ->send(); + + $record->refresh(); + }), + ]), TextInput::make('uuid') ->label(trans('server/setting.server_info.uuid')) ->columnSpan([ @@ -97,14 +263,14 @@ class Settings extends ServerFormPage ->label(trans('server/setting.server_info.limits.title')) ->columnSpan([ 'default' => 1, - 'sm' => 2, - 'md' => 2, + 'sm' => 1, + 'md' => 4, 'lg' => 6, ]) ->columns([ 'default' => 1, 'sm' => 1, - 'md' => 1, + 'md' => 2, 'lg' => 3, ]) ->schema([ diff --git a/app/Livewire/ServerEntry.php b/app/Livewire/ServerEntry.php index 1abcccc86..3f691084e 100644 --- a/app/Livewire/ServerEntry.php +++ b/app/Livewire/ServerEntry.php @@ -2,7 +2,9 @@ namespace App\Livewire; +use App\Filament\Server\Pages\Console; use App\Models\Server; +use Filament\Support\Facades\FilamentView; use Illuminate\View\View; use Livewire\Component; @@ -12,68 +14,23 @@ class ServerEntry extends Component public function render(): View { - return view('livewire.server-entry'); + return view('livewire.server-entry', ['component' => $this]); } - public function placeholder(): string + public function placeholder(): View { - return <<<'HTML' -
-
-
+ return view('livewire.server-entry-placeholder', ['server' => $this->server, 'component' => $this]); + } -
- @if($server->egg->image) -
- @endif + public function redirectUrl(?bool $shouldOpenUrlInNewTab = false): string + { + $url = Console::getUrl(panel: 'server', tenant: $this->server); + $target = $shouldOpenUrlInNewTab ? '_blank' : '_self'; -
- -

- {{ $server->name }} - - ({{ trans('server/dashboard.loading') }}) - -

-
+ if (!$shouldOpenUrlInNewTab && FilamentView::hasSpaMode($url)) { + return sprintf("Livewire.navigate('%s')", $url); + } -
-
-

{{ trans('server/dashboard.cpu') }}

-

{{ format_number(0, precision: 2) . '%' }}

-
-

{{ $server->formatResource(\App\Enums\ServerResourceType::CPULimit) }}

-
-
-

{{ trans('server/dashboard.memory') }}

-

{{ convert_bytes_to_readable(0, decimals: 2) }}

-
-

{{ $server->formatResource(\App\Enums\ServerResourceType::MemoryLimit) }}

-
-
-

{{ trans('server/dashboard.disk') }}

-

{{ convert_bytes_to_readable(0, decimals: 2) }}

-
-

{{ $server->formatResource(\App\Enums\ServerResourceType::DiskLimit) }}

-
- -
-
-
- HTML; + return sprintf("window.open('%s', '%s')", $url, $target); } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 098958c99..b501ef5b6 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -12,6 +12,7 @@ use App\Services\Subusers\SubuserDeletionService; use App\Traits\HasValidation; use Carbon\CarbonInterface; use Database\Factories\ServerFactory; +use Filament\Models\Contracts\HasAvatar; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Collection; @@ -55,6 +56,7 @@ use Psr\Http\Message\ResponseInterface; * @property int $egg_id * @property string $startup * @property string $image + * @property string|null $icon * @property int|null $allocation_limit * @property int|null $database_limit * @property int|null $backup_limit @@ -70,7 +72,7 @@ use Psr\Http\Message\ResponseInterface; * @property int|null $backups_count * @property Collection|Database[] $databases * @property int|null $databases_count - * @property Egg|null $egg + * @property Egg $egg * @property Collection|Mount[] $mounts * @property int|null $mounts_count * @property Node $node @@ -129,7 +131,7 @@ use Psr\Http\Message\ResponseInterface; * @method static Builder|Server wherePorts($value) * @method static Builder|Server whereUuidShort($value) */ -class Server extends Model implements Validatable +class Server extends Model implements HasAvatar, Validatable { use HasFactory; use HasValidation; @@ -181,6 +183,7 @@ class Server extends Model implements Validatable 'startup' => ['required', 'string'], 'skip_scripts' => ['sometimes', 'boolean'], 'image' => ['required', 'string', 'max:255'], + 'icon' => ['sometimes', 'nullable', 'string'], 'database_limit' => ['present', 'nullable', 'integer', 'min:0'], 'allocation_limit' => ['sometimes', 'nullable', 'integer', 'min:0'], 'backup_limit' => ['present', 'nullable', 'integer', 'min:0'], @@ -513,4 +516,10 @@ class Server extends Model implements Validatable get: fn () => $this->status ?? $this->retrieveStatus(), ); } + + public function getFilamentAvatarUrl(): ?string + { + return $this->icon ?? $this->egg->image; + + } } diff --git a/database/migrations/2025_11_16_000000_add_icon_to_servers.php b/database/migrations/2025_11_16_000000_add_icon_to_servers.php new file mode 100644 index 000000000..e89799d0f --- /dev/null +++ b/database/migrations/2025_11_16_000000_add_icon_to_servers.php @@ -0,0 +1,28 @@ +longText('icon')->nullable()->after('image'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('icon'); + }); + } +}; diff --git a/lang/en/server/setting.php b/lang/en/server/setting.php index c8ee66d57..8691baebe 100644 --- a/lang/en/server/setting.php +++ b/lang/en/server/setting.php @@ -14,6 +14,12 @@ return [ 'uuid' => 'Server UUID', 'uuid_short' => 'Server ID', 'node_name' => 'Node Name', + 'icon' => [ + 'upload' => 'Upload Icon', + 'tooltip' => 'Using Egg Icon', + 'updated' => 'Server icon updated', + 'deleted' => 'Server icon deleted', + ], 'limits' => [ 'title' => 'Limits', 'unlimited' => 'Unlimited', diff --git a/resources/views/livewire/server-entry-placeholder.blade.php b/resources/views/livewire/server-entry-placeholder.blade.php new file mode 100644 index 000000000..7668d53d4 --- /dev/null +++ b/resources/views/livewire/server-entry-placeholder.blade.php @@ -0,0 +1,69 @@ +@php + $backgroundImage = $server->icon ?? $server->egg->image; +@endphp + +
+
+ +
+ @if($backgroundImage) +
+ @endif + +
!$server->description, + ])> + + +

+ {{ $server->name }} + ({{ trans('server/dashboard.loading') }}) +

+
+ + @if ($server->description) +
+

{{ Str::limit($server->description, 40, preserveWords: true) }}

+
+ @endif + + +
+
+

{{ trans('server/dashboard.cpu') }}

+

{{ format_number(0, precision: 2) . '%' }}

+
+

{{ $server->formatResource(\App\Enums\ServerResourceType::CPULimit) }}

+
+
+

{{ trans('server/dashboard.memory') }}

+

{{ convert_bytes_to_readable(0, decimals: 2) }}

+
+

{{ $server->formatResource(\App\Enums\ServerResourceType::MemoryLimit) }}

+
+
+

{{ trans('server/dashboard.disk') }}

+

{{ convert_bytes_to_readable(0, decimals: 2) }}

+
+

{{ $server->formatResource(\App\Enums\ServerResourceType::DiskLimit) }}

+
+ +
+
+
+ diff --git a/resources/views/livewire/server-entry.blade.php b/resources/views/livewire/server-entry.blade.php index 85c207bb1..f4b706c75 100644 --- a/resources/views/livewire/server-entry.blade.php +++ b/resources/views/livewire/server-entry.blade.php @@ -1,25 +1,27 @@ @php $actiongroup = \App\Filament\App\Resources\Servers\Pages\ListServers::getPowerActionGroup()->record($server); + $backgroundImage = $server->icon ?? $server->egg->image; @endphp
+ x-on:click="{{ $component->redirectUrl() }}" + x-on:auxclick.prevent="if ($event.button === 1) {{ $component->redirectUrl(true) }}">
- @if($server->egg->image) + @if($backgroundImage)
+ ">
@endif
@if ($server->description) -
-

{{ Str::limit($server->description, 40, preserveWords: true) }}

-
+
+

{{ Str::limit($server->description, 40, preserveWords: true) }}

+
@endif