Add server icons (#1906)

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
Charles 2025-11-21 16:48:20 -05:00 committed by GitHub
parent a195b56f93
commit 65bb99e2b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 599 additions and 178 deletions

View File

@ -29,6 +29,7 @@ use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Forms\Components\CodeEditor; use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Repeater;
@ -38,12 +39,14 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\StateCasts\BooleanStateCast; use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab; 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\Components\Utilities\Set;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment; use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -94,90 +98,265 @@ class EditServer extends EditRecord
->label(trans('admin/server.tabs.information')) ->label(trans('admin/server.tabs.information'))
->icon('tabler-info-circle') ->icon('tabler-info-circle')
->schema([ ->schema([
TextInput::make('name') Grid::make()
->prefixIcon('tabler-server') ->columns(2)
->label(trans('admin/server.name')) ->columnStart(1)
->suffixAction(Action::make('random') ->schema([
->icon('tabler-dice-' . random_int(1, 6)) Image::make('', 'icon')
->action(function (Set $set, Get $get) { ->hidden(fn ($record) => !$record->icon && !$record->egg->image)
$egg = Egg::find($get('egg_id')); ->url(fn ($record) => $record->icon ?: $record->egg->image)
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : ''; ->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
->columnSpan(2)
$word = (new RandomWordService())->word(); ->alignJustify(),
Action::make('uploadIcon')
$set('name', $prefix . $word); ->iconButton()->iconSize(IconSize::Large)
})) ->icon('tabler-photo-up')
->columnSpan([ ->modal()
'default' => 2, ->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
'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([ ->schema([
CodeEditor::make('logs') Tabs::make()->tabs([
->hiddenLabel() Tab::make(trans('admin/egg.import.url'))
->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) { ->schema([
try { Hidden::make('base64Image'),
$logs = $serverRepository->setServer($server)->getInstallLogs(); 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']); return;
} 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 ''; 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') Textarea::make('description')
->label(trans('admin/server.description')) ->label(trans('admin/server.description'))
->columnSpanFull(), ->columnSpanFull(),
TextInput::make('uuid') TextInput::make('uuid')
->label(trans('admin/server.uuid')) ->label(trans('admin/server.uuid'))
->copyable() ->copyable()

View File

@ -20,6 +20,7 @@ use Filament\Schemas\Components\Tabs\Tab;
use Filament\Support\Enums\Alignment; use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize; use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\Column; use Filament\Tables\Columns\Column;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\Layout\Stack; use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
@ -62,6 +63,10 @@ class ListServers extends ListRecords
protected function tableColumns(): array protected function tableColumns(): array
{ {
return [ return [
ImageColumn::make('icon')
->label('')
->imageSize(46)
->state(fn (Server $server) => $server->icon ?: $server->egg->image),
TextColumn::make('condition') TextColumn::make('condition')
->label(trans('server/dashboard.status')) ->label(trans('server/dashboard.status'))
->badge() ->badge()

View File

@ -8,14 +8,23 @@ use App\Models\Server;
use App\Services\Servers\ReinstallServerService; use App\Services\Servers\ReinstallServerService;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Section; 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\Schemas\Schema;
use Filament\Support\Enums\Alignment; use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
class Settings extends ServerFormPage class Settings extends ServerFormPage
{ {
@ -29,51 +38,208 @@ class Settings extends ServerFormPage
public function form(Schema $schema): Schema public function form(Schema $schema): Schema
{ {
return parent::form($schema) return parent::form($schema)
->columns(4)
->components([ ->components([
Section::make(trans('server/setting.server_info.title')) Section::make(trans('server/setting.server_info.title'))
->columnSpanFull() ->columnSpanFull()
->columns([ ->columns([
'default' => 1, 'default' => 1,
'sm' => 2, 'sm' => 1,
'md' => 4, 'md' => 4,
'lg' => 6, 'lg' => 6,
]) ])
->schema([ ->schema([
Fieldset::make() Fieldset::make()
->label(trans('server/setting.server_info.information')) ->label(trans('server/setting.server_info.information'))
->columnSpan([ ->columnSpanFull()
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 6,
])
->schema([ ->schema([
TextInput::make('name') Grid::make()
->label(trans('server/setting.server_info.name')) ->columns(2)
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_RENAME, $server)) ->columnSpan(5)
->required() ->schema([
->columnSpan([ TextInput::make('name')
'default' => 1, ->columnStart(1)
'sm' => 2, ->columnSpanFull()
'md' => 2, ->label(trans('server/setting.server_info.name'))
'lg' => 6, ->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_RENAME, $server))
]) ->required()
->live(onBlur: true) ->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)), ->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)),
Textarea::make('description') Textarea::make('description')
->label(trans('server/setting.server_info.description')) ->columnStart(1)
->hidden(!config('panel.editable_server_descriptions')) ->columnSpanFull()
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_DESCRIPTION, $server)) ->label(trans('server/setting.server_info.description'))
->columnSpan([ ->hidden(!config('panel.editable_server_descriptions'))
'default' => 1, ->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_DESCRIPTION, $server))
'sm' => 2, ->autosize()
'md' => 2, ->live(onBlur: true)
'lg' => 6, ->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
]) ]),
->autosize() Grid::make()
->live(onBlur: true) ->columns(2)
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)), ->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') TextInput::make('uuid')
->label(trans('server/setting.server_info.uuid')) ->label(trans('server/setting.server_info.uuid'))
->columnSpan([ ->columnSpan([
@ -97,14 +263,14 @@ class Settings extends ServerFormPage
->label(trans('server/setting.server_info.limits.title')) ->label(trans('server/setting.server_info.limits.title'))
->columnSpan([ ->columnSpan([
'default' => 1, 'default' => 1,
'sm' => 2, 'sm' => 1,
'md' => 2, 'md' => 4,
'lg' => 6, 'lg' => 6,
]) ])
->columns([ ->columns([
'default' => 1, 'default' => 1,
'sm' => 1, 'sm' => 1,
'md' => 1, 'md' => 2,
'lg' => 3, 'lg' => 3,
]) ])
->schema([ ->schema([

View File

@ -2,7 +2,9 @@
namespace App\Livewire; namespace App\Livewire;
use App\Filament\Server\Pages\Console;
use App\Models\Server; use App\Models\Server;
use Filament\Support\Facades\FilamentView;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Component; use Livewire\Component;
@ -12,68 +14,23 @@ class ServerEntry extends Component
public function render(): View 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]);
<div class="relative cursor-pointer" x-on:click="window.location.href = '{{ \App\Filament\Server\Pages\Console::getUrl(panel: 'server', tenant: $server) }}'"> }
<div
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
style="background-color: #D97706;">
</div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3"> public function redirectUrl(?bool $shouldOpenUrlInNewTab = false): string
@if($server->egg->image) {
<div style=" $url = Console::getUrl(panel: 'server', tenant: $this->server);
position: absolute; $target = $shouldOpenUrlInNewTab ? '_blank' : '_self';
inset: 0;
background: url('{{ $server->egg->image }}') right no-repeat;
background-size: contain;
opacity: 0.20;
max-width: 680px;
max-height: 140px;
"></div>
@endif
<div class="flex items-center mb-5 gap-2"> if (!$shouldOpenUrlInNewTab && FilamentView::hasSpaMode($url)) {
<x-filament::loading-indicator class="h-6 w-6" /> return sprintf("Livewire.navigate('%s')", $url);
<h2 class="text-xl font-bold"> }
{{ $server->name }}
<span class="dark:text-gray-400">
({{ trans('server/dashboard.loading') }})
</span>
</h2>
</div>
<div class="flex justify-between text-center items-center gap-4"> return sprintf("window.open('%s', '%s')", $url, $target);
<div>
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.cpu') }}</p>
<p class="text-md font-semibold">{{ format_number(0, precision: 2) . '%' }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::CPULimit) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.memory') }}</p>
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::MemoryLimit) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.disk') }}</p>
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::DiskLimit) }}</p>
</div>
<div class="hidden sm:block">
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.network') }}</p>
<hr class="p-0.5">
<p class="text-md font-semibold">{{ $server->allocation?->address ?? trans('server/dashboard.none') }} </p>
</div>
</div>
</div>
</div>
HTML;
} }
} }

View File

@ -12,6 +12,7 @@ use App\Services\Subusers\SubuserDeletionService;
use App\Traits\HasValidation; use App\Traits\HasValidation;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Database\Factories\ServerFactory; use Database\Factories\ServerFactory;
use Filament\Models\Contracts\HasAvatar;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
@ -55,6 +56,7 @@ use Psr\Http\Message\ResponseInterface;
* @property int $egg_id * @property int $egg_id
* @property string $startup * @property string $startup
* @property string $image * @property string $image
* @property string|null $icon
* @property int|null $allocation_limit * @property int|null $allocation_limit
* @property int|null $database_limit * @property int|null $database_limit
* @property int|null $backup_limit * @property int|null $backup_limit
@ -70,7 +72,7 @@ use Psr\Http\Message\ResponseInterface;
* @property int|null $backups_count * @property int|null $backups_count
* @property Collection|Database[] $databases * @property Collection|Database[] $databases
* @property int|null $databases_count * @property int|null $databases_count
* @property Egg|null $egg * @property Egg $egg
* @property Collection|Mount[] $mounts * @property Collection|Mount[] $mounts
* @property int|null $mounts_count * @property int|null $mounts_count
* @property Node $node * @property Node $node
@ -129,7 +131,7 @@ use Psr\Http\Message\ResponseInterface;
* @method static Builder|Server wherePorts($value) * @method static Builder|Server wherePorts($value)
* @method static Builder|Server whereUuidShort($value) * @method static Builder|Server whereUuidShort($value)
*/ */
class Server extends Model implements Validatable class Server extends Model implements HasAvatar, Validatable
{ {
use HasFactory; use HasFactory;
use HasValidation; use HasValidation;
@ -181,6 +183,7 @@ class Server extends Model implements Validatable
'startup' => ['required', 'string'], 'startup' => ['required', 'string'],
'skip_scripts' => ['sometimes', 'boolean'], 'skip_scripts' => ['sometimes', 'boolean'],
'image' => ['required', 'string', 'max:255'], 'image' => ['required', 'string', 'max:255'],
'icon' => ['sometimes', 'nullable', 'string'],
'database_limit' => ['present', 'nullable', 'integer', 'min:0'], 'database_limit' => ['present', 'nullable', 'integer', 'min:0'],
'allocation_limit' => ['sometimes', 'nullable', 'integer', 'min:0'], 'allocation_limit' => ['sometimes', 'nullable', 'integer', 'min:0'],
'backup_limit' => ['present', '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(), get: fn () => $this->status ?? $this->retrieveStatus(),
); );
} }
public function getFilamentAvatarUrl(): ?string
{
return $this->icon ?? $this->egg->image;
}
} }

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->longText('icon')->nullable()->after('image');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('icon');
});
}
};

View File

@ -14,6 +14,12 @@ return [
'uuid' => 'Server UUID', 'uuid' => 'Server UUID',
'uuid_short' => 'Server ID', 'uuid_short' => 'Server ID',
'node_name' => 'Node Name', 'node_name' => 'Node Name',
'icon' => [
'upload' => 'Upload Icon',
'tooltip' => 'Using Egg Icon',
'updated' => 'Server icon updated',
'deleted' => 'Server icon deleted',
],
'limits' => [ 'limits' => [
'title' => 'Limits', 'title' => 'Limits',
'unlimited' => 'Unlimited', 'unlimited' => 'Unlimited',

View File

@ -0,0 +1,69 @@
@php
$backgroundImage = $server->icon ?? $server->egg->image;
@endphp
<div class="relative cursor-pointer"
x-on:click="{{ $component->redirectUrl() }}"
x-on:auxclick.prevent="if ($event.button === 1) {{ $component->redirectUrl(true) }}">
<div class="absolute left-0 top-1 bottom-0 w-1 rounded-lg" style="background-color: #D97706;"></div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
@if($backgroundImage)
<div style="
position: absolute;
inset: 0;
background: url('{{ $backgroundImage }}') right no-repeat;
background-size: contain;
opacity: 0.20;
max-width: 680px;
max-height: 140px;
"></div>
@endif
<div @class([
'flex items-center gap-2',
'mb-5' => !$server->description,
])>
<x-filament::loading-indicator class="h-6 w-6" />
<h2 class="text-xl font-bold">
{{ $server->name }}
<span class="dark:text-gray-400">({{ trans('server/dashboard.loading') }})</span>
</h2>
</div>
@if ($server->description)
<div class="text-left mb-1 ml-4 pl-4">
<p class="text-base dark:text-gray-400">{{ Str::limit($server->description, 40, preserveWords: true) }}</p>
</div>
@endif
<div class="flex justify-between text-center items-center gap-4">
<div>
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.cpu') }}</p>
<p class="text-md font-semibold">{{ format_number(0, precision: 2) . '%' }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::CPULimit) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.memory') }}</p>
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::MemoryLimit) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.disk') }}</p>
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::DiskLimit) }}</p>
</div>
<div class="hidden sm:block">
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.network') }}</p>
<hr class="p-0.5">
<p class="text-md font-semibold">{{ $server->allocation?->address ?? trans('server/dashboard.none') }}</p>
</div>
</div>
</div>
</div>

View File

@ -1,25 +1,27 @@
@php @php
$actiongroup = \App\Filament\App\Resources\Servers\Pages\ListServers::getPowerActionGroup()->record($server); $actiongroup = \App\Filament\App\Resources\Servers\Pages\ListServers::getPowerActionGroup()->record($server);
$backgroundImage = $server->icon ?? $server->egg->image;
@endphp @endphp
<div wire:poll.15s <div wire:poll.15s
class="relative cursor-pointer" class="relative cursor-pointer"
x-on:click="window.location.href = '{{ \App\Filament\Server\Pages\Console::getUrl(panel: 'server', tenant: $server) }}'"> x-on:click="{{ $component->redirectUrl() }}"
x-on:auxclick.prevent="if ($event.button === 1) {{ $component->redirectUrl(true) }}">
<div class="absolute left-0 top-1 bottom-0 w-1 rounded-lg" <div class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
style="background-color: {{ $server->condition->getColor(true) }};"> style="background-color: {{ $server->condition->getColor(true) }};">
</div> </div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3"> <div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
@if($server->egg->image) @if($backgroundImage)
<div style=" <div style="
position: absolute; position: absolute;
inset: 0; inset: 0;
background: url('{{ $server->egg->image }}') right no-repeat; background: url('{{ $backgroundImage }}') right no-repeat;
background-size: contain; background-size: contain;
opacity: 0.20; opacity: 0.20;
max-width: 680px; max-width: 680px;
max-height: 140px; max-height: 140px;
"></div> "></div>
@endif @endif
<div @class([ <div @class([
@ -49,9 +51,9 @@
</div> </div>
@if ($server->description) @if ($server->description)
<div class="text-left mb-1 ml-4 pl-4"> <div class="text-left mb-1 ml-4 pl-4">
<p class="text-base text-gray-400">{{ Str::limit($server->description, 40, preserveWords: true) }}</p> <p class="text-base dark:text-gray-400">{{ Str::limit($server->description, 40, preserveWords: true) }}</p>
</div> </div>
@endif @endif
<div class="flex justify-between text-center items-center gap-4"> <div class="flex justify-between text-center items-center gap-4">