mirror of
https://github.com/pelican-dev/panel.git
synced 2025-12-08 18:30:15 +01:00
Add server icons (#1906)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
This commit is contained in:
parent
a195b56f93
commit
65bb99e2b0
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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'
|
||||
<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>
|
||||
return view('livewire.server-entry-placeholder', ['server' => $this->server, 'component' => $this]);
|
||||
}
|
||||
|
||||
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
|
||||
@if($server->egg->image)
|
||||
<div style="
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: url('{{ $server->egg->image }}') right no-repeat;
|
||||
background-size: contain;
|
||||
opacity: 0.20;
|
||||
max-width: 680px;
|
||||
max-height: 140px;
|
||||
"></div>
|
||||
@endif
|
||||
public function redirectUrl(?bool $shouldOpenUrlInNewTab = false): string
|
||||
{
|
||||
$url = Console::getUrl(panel: 'server', tenant: $this->server);
|
||||
$target = $shouldOpenUrlInNewTab ? '_blank' : '_self';
|
||||
|
||||
<div class="flex items-center mb-5 gap-2">
|
||||
<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 (!$shouldOpenUrlInNewTab && FilamentView::hasSpaMode($url)) {
|
||||
return sprintf("Livewire.navigate('%s')", $url);
|
||||
}
|
||||
|
||||
<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>
|
||||
HTML;
|
||||
return sprintf("window.open('%s', '%s')", $url, $target);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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',
|
||||
|
||||
69
resources/views/livewire/server-entry-placeholder.blade.php
Normal file
69
resources/views/livewire/server-entry-placeholder.blade.php
Normal 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>
|
||||
|
||||
@ -1,25 +1,27 @@
|
||||
@php
|
||||
$actiongroup = \App\Filament\App\Resources\Servers\Pages\ListServers::getPowerActionGroup()->record($server);
|
||||
$backgroundImage = $server->icon ?? $server->egg->image;
|
||||
@endphp
|
||||
<div wire:poll.15s
|
||||
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"
|
||||
style="background-color: {{ $server->condition->getColor(true) }};">
|
||||
</div>
|
||||
|
||||
<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="
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: url('{{ $server->egg->image }}') right no-repeat;
|
||||
background: url('{{ $backgroundImage }}') right no-repeat;
|
||||
background-size: contain;
|
||||
opacity: 0.20;
|
||||
max-width: 680px;
|
||||
max-height: 140px;
|
||||
"></div>
|
||||
"></div>
|
||||
@endif
|
||||
|
||||
<div @class([
|
||||
@ -49,9 +51,9 @@
|
||||
</div>
|
||||
|
||||
@if ($server->description)
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user