mirror of
https://github.com/pelican-dev/panel.git
synced 2026-02-28 11:20:40 +03:00
Compare commits
58 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd7a01aa04 | ||
|
|
55badb5644 | ||
|
|
93f059025c | ||
|
|
7be0cd6928 | ||
|
|
0156456919 | ||
|
|
b9d1ce4438 | ||
|
|
9ce262bf56 | ||
|
|
7ee52affb2 | ||
|
|
93bfe925b9 | ||
|
|
cc1ac1eba1 | ||
|
|
02d24b8a36 | ||
|
|
16fac3b5c6 | ||
|
|
eb99f53d87 | ||
|
|
643e4168b9 | ||
|
|
51cd7a8e81 | ||
|
|
91bf38b63d | ||
|
|
e3699f34d8 | ||
|
|
dc3da2dc98 | ||
|
|
d245751c97 | ||
|
|
e0d7a094ab | ||
|
|
3010e3d61e | ||
|
|
d68e7218a8 | ||
|
|
a4435a7454 | ||
|
|
df26c4f9f5 | ||
|
|
6f1de67523 | ||
|
|
6f009ee126 | ||
|
|
328e159c6b | ||
|
|
f9fd426aca | ||
|
|
6166fac929 | ||
|
|
4bd1070025 | ||
|
|
2d6e30b646 | ||
|
|
f61c6b9dc2 | ||
|
|
5e29737dc5 | ||
|
|
d996019204 | ||
|
|
91d8dbd084 | ||
|
|
bb03ddda50 | ||
|
|
1c66681c0e | ||
|
|
0728266826 | ||
|
|
d81c9faac6 | ||
|
|
cff54f1969 | ||
|
|
201563a13b | ||
|
|
8f2261f6cd | ||
|
|
29cc92f0dc | ||
|
|
33f10cbcb9 | ||
|
|
b538532e34 | ||
|
|
a892821b4f | ||
|
|
5a3b50b31f | ||
|
|
51b217571b | ||
|
|
6e75c76c60 | ||
|
|
e22c5c3e0a | ||
|
|
f3171939a4 | ||
|
|
189d564f87 | ||
|
|
7926f97c8e | ||
|
|
f4d39c1c68 | ||
|
|
6c2d0a2d50 | ||
|
|
f6899301fd | ||
|
|
cbb4ef1da2 | ||
|
|
f6ef76d98e |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
|
||||
@@ -118,7 +118,9 @@ class AppSettingsCommand extends Command
|
||||
}
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
|
||||
Artisan::call('p:environment:queue-service', $redisUsed ? ['--use-redis'] : []);
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--use-redis' => $redisUsed,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->info($this->console->output());
|
||||
@@ -127,7 +129,7 @@ class AppSettingsCommand extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Request connection details and verify them.
|
||||
* Request redis connection details and verify them.
|
||||
*/
|
||||
private function requestRedisSettings(): void
|
||||
{
|
||||
|
||||
@@ -98,7 +98,7 @@ class DatabaseSettingsCommand extends Command
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
|
||||
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||
'Database Path',
|
||||
config('database.connections.sqlite.database', database_path('database.sqlite'))
|
||||
env('DB_DATABASE', 'database.sqlite')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,17 +19,18 @@ class QueueWorkerServiceCommand extends Command
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$serviceName = $this->option('service-name') ?? $this->ask('Service name', 'pelican-queue');
|
||||
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
|
||||
$path = '/etc/systemd/system/' . $serviceName . '.service';
|
||||
|
||||
if (file_exists($path) && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
|
||||
$this->line('Creation of queue worker service file aborted.');
|
||||
$fileExists = file_exists($path);
|
||||
if ($fileExists && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
|
||||
$this->line('Creation of queue worker service file aborted because serive file already exists.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->option('user') ?? $this->ask('User', 'www-data');
|
||||
$group = $this->option('group') ?? $this->ask('Group', 'www-data');
|
||||
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
|
||||
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
|
||||
|
||||
$afterRedis = $this->option('use-redis') ? '\nAfter=redis-server.service' : '';
|
||||
|
||||
@@ -45,7 +46,7 @@ Description=Pelican Queue Service$afterRedis
|
||||
User=$user
|
||||
Group=$group
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/php $basePath/artisan queue:work --queue=high,standard,low --tries=3
|
||||
ExecStart=/usr/bin/php $basePath/artisan queue:work --tries=3
|
||||
StartLimitInterval=180
|
||||
StartLimitBurst=30
|
||||
RestartSec=5s
|
||||
@@ -60,13 +61,24 @@ WantedBy=multi-user.target
|
||||
return;
|
||||
}
|
||||
|
||||
$result = Process::run("systemctl enable --now $serviceName.service");
|
||||
if ($result->failed()) {
|
||||
$this->error('Error enabling service: ' . $result->errorOutput());
|
||||
if ($fileExists) {
|
||||
$result = Process::run("systemctl restart $serviceName.service");
|
||||
if ($result->failed()) {
|
||||
$this->error('Error restarting service: ' . $result->errorOutput());
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line('Queue worker service file updated successfully.');
|
||||
} else {
|
||||
$result = Process::run("systemctl enable --now $serviceName.service");
|
||||
if ($result->failed()) {
|
||||
$this->error('Error enabling service: ' . $result->errorOutput());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line('Queue worker service file created successfully.');
|
||||
}
|
||||
|
||||
$this->line('Queue worker service file created successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class InfoCommand extends Command
|
||||
{
|
||||
$this->output->title('Version Information');
|
||||
$this->table([], [
|
||||
['Panel Version', config('app.version')],
|
||||
['Panel Version', $this->versionService->versionData()['version']],
|
||||
['Latest Version', $this->versionService->getPanel()],
|
||||
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
|
||||
], 'compact');
|
||||
|
||||
@@ -25,6 +25,7 @@ class MakeNodeCommand extends Command
|
||||
{--uploadSize= : Enter the maximum upload filesize.}
|
||||
{--daemonListeningPort= : Enter the daemon listening port.}
|
||||
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
|
||||
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
|
||||
{--daemonBase= : Enter the base folder.}';
|
||||
|
||||
protected $description = 'Creates a new node on the system via the CLI.';
|
||||
@@ -65,6 +66,7 @@ class MakeNodeCommand extends Command
|
||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100');
|
||||
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
|
||||
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');
|
||||
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(__('commands.make_node.daemonSFTPAlias'), '');
|
||||
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
|
||||
|
||||
$node = $this->creationService->handle($data);
|
||||
|
||||
@@ -24,7 +24,7 @@ class ProcessRunnableCommand extends Command
|
||||
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
|
||||
->where('is_active', true)
|
||||
->where('is_processing', false)
|
||||
->whereDate('next_run_at', '<=', Carbon::now()->toDateString())
|
||||
->whereDate('next_run_at', '<=', Carbon::now()->toDateTimeString())
|
||||
->get();
|
||||
|
||||
if ($schedules->count() < 1) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class MakeUserCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
try {
|
||||
DB::select('select 1 where 1');
|
||||
DB::connection()->getPdo();
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ use App\Exceptions\DisplayException;
|
||||
|
||||
class TwoFactorAuthenticationTokenInvalid extends DisplayException
|
||||
{
|
||||
/**
|
||||
* TwoFactorAuthenticationTokenInvalid constructor.
|
||||
*/
|
||||
public string $title = 'Invalid 2FA Code';
|
||||
public string $icon = 'tabler-2fa';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The provided two-factor authentication token was not valid.');
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
@@ -29,8 +30,14 @@ class Dashboard extends Page
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
/** @var SoftwareVersionService $softwareVersionService */
|
||||
$softwareVersionService = app(SoftwareVersionService::class);
|
||||
|
||||
return [
|
||||
'inDevelopment' => config('app.version') === 'canary',
|
||||
'version' => $softwareVersionService->versionData()['version'],
|
||||
'latestVersion' => $softwareVersionService->getPanel(),
|
||||
'isLatest' => $softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
@@ -43,6 +50,13 @@ class Dashboard extends Page
|
||||
->icon('tabler-brand-github')
|
||||
->url('https://github.com/pelican-dev/panel/discussions', true),
|
||||
],
|
||||
'updateActions' => [
|
||||
CreateAction::make()
|
||||
->label('Read Documentation')
|
||||
->icon('tabler-clipboard-text')
|
||||
->url('https://pelican.dev/docs/panel/update', true)
|
||||
->color('warning'),
|
||||
],
|
||||
'nodeActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
|
||||
@@ -53,7 +67,7 @@ class Dashboard extends Page
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->url($softwareVersionService->getDonations(), true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Models\DatabaseHost;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms;
|
||||
@@ -71,7 +72,9 @@ class EditDatabaseHost extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
Actions\DeleteAction::make()
|
||||
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? 'Database Host Has Databases' : 'Delete')
|
||||
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Table;
|
||||
@@ -62,21 +63,58 @@ class ListEggs extends ListRecords
|
||||
Actions\Action::make('import')
|
||||
->label('Import')
|
||||
->form([
|
||||
Forms\Components\FileUpload::make('egg')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false)
|
||||
->multiple(),
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tabs\Tab::make('From File')
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
Forms\Components\FileUpload::make('egg')
|
||||
->label('Egg')
|
||||
->hint('This should be the json file ( egg-minecraft.json )')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false)
|
||||
->multiple(),
|
||||
]),
|
||||
Tabs\Tab::make('From URL')
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('url')
|
||||
->label('URL')
|
||||
->hint('This URL should point to a single json file')
|
||||
->url(),
|
||||
]),
|
||||
])
|
||||
->contained(false),
|
||||
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
/** @var TemporaryUploadedFile $eggFile */
|
||||
$eggFile = $data['egg'];
|
||||
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
foreach ($eggFile as $file) {
|
||||
if (!empty($data['egg'])) {
|
||||
/** @var TemporaryUploadedFile[] $eggFile */
|
||||
$eggFile = $data['egg'];
|
||||
|
||||
foreach ($eggFile as $file) {
|
||||
try {
|
||||
$eggImportService->fromFile($file);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['url'])) {
|
||||
try {
|
||||
$eggImportService->handle($file);
|
||||
$eggImportService->fromUrl($data['url']);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
|
||||
@@ -215,6 +215,18 @@ class EditNode extends EditRecord
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix('MiB'),
|
||||
Forms\Components\TextInput::make('daemon_sftp')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('SFTP Port')
|
||||
->minValue(0)
|
||||
->maxValue(65536)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
Forms\Components\TextInput::make('daemon_sftp_alias')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('SFTP Alias')
|
||||
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
|
||||
Forms\Components\ToggleButtons::make('public')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('Automatic Allocation')->inline()
|
||||
|
||||
@@ -113,7 +113,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
foreach (range($start, $end) as $i) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,19 +371,20 @@ class CreateServer extends CreateRecord
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->maxLength(191)
|
||||
->rules([
|
||||
->required(fn (Forms\Get $get) => in_array('required', explode('|', $get('rules'))))
|
||||
->rules(
|
||||
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $get('rules'),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'));
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'))->toString();
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
]);
|
||||
);
|
||||
|
||||
$select = Forms\Components\Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
|
||||
@@ -532,7 +532,6 @@ class EditServer extends EditRecord
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->maxLength(191)
|
||||
->rules([
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
@@ -577,6 +576,7 @@ class EditServer extends EditRecord
|
||||
->options(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
|
||||
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
|
||||
->label('Mounts')
|
||||
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Tabs\Tab::make('Databases')
|
||||
|
||||
@@ -1,588 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Services\Servers\ServerDeletionService;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Closure;
|
||||
|
||||
class EditServerOrg extends EditRecord
|
||||
{
|
||||
protected static string $resource = ServerResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('docker')
|
||||
->label('Container Status')->inline()->inlineLabel()
|
||||
->formatStateUsing(function ($state, Server $server) {
|
||||
if ($server->node_id === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/** @var DaemonServerRepository $service */
|
||||
$service = resolve(DaemonServerRepository::class);
|
||||
$details = $service->setServer($server)->getDetails();
|
||||
|
||||
return $details['state'] ?? 'unknown';
|
||||
})
|
||||
->options(fn ($state) => collect(ContainerStatus::cases())->filter(fn ($containerStatus) => $containerStatus->value === $state)->mapWithKeys(
|
||||
fn (ContainerStatus $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
|
||||
))
|
||||
->colors(collect(ContainerStatus::cases())->mapWithKeys(
|
||||
fn (ContainerStatus $status) => [$status->value => $status->color()]
|
||||
))
|
||||
->icons(collect(ContainerStatus::cases())->mapWithKeys(
|
||||
fn (ContainerStatus $status) => [$status->value => $status->icon()]
|
||||
))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
]),
|
||||
|
||||
Forms\Components\ToggleButtons::make('status')
|
||||
->label('Server State')->inline()->inlineLabel()
|
||||
->helperText('')
|
||||
|
||||
->formatStateUsing(fn ($state) => $state ?? ServerState::Normal)
|
||||
->options(fn ($state) => collect(ServerState::cases())->filter(fn ($serverState) => $serverState->value === $state)->mapWithKeys(
|
||||
fn (ServerState $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
|
||||
))
|
||||
->colors(collect(ServerState::cases())->mapWithKeys(
|
||||
fn (ServerState $state) => [$state->value => $state->color()]
|
||||
))
|
||||
->icons(collect(ServerState::cases())->mapWithKeys(
|
||||
fn (ServerState $state) => [$state->value => $state->icon()]
|
||||
))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('external_id')
|
||||
->maxLength(191)
|
||||
->hidden(),
|
||||
|
||||
Forms\Components\TextInput::make('name')
|
||||
->prefixIcon('tabler-server')
|
||||
->label('Display Name')
|
||||
->suffixAction(Forms\Components\Actions\Action::make('random')
|
||||
->icon('tabler-dice-' . random_int(1, 6))
|
||||
->action(function (Forms\Set $set, Forms\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' => 4,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->required()
|
||||
->maxLength(191),
|
||||
|
||||
Forms\Components\Select::make('owner_id')
|
||||
->prefixIcon('tabler-user')
|
||||
->label('Owner')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->relationship('user', 'username')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->hidden()
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\Select::make('egg_id')
|
||||
->disabledOn('edit')
|
||||
->prefixIcon('tabler-egg')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 5,
|
||||
])
|
||||
->relationship('egg', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||
->label('Run Egg Install Script?')->inline()
|
||||
->options([
|
||||
false => 'Yes',
|
||||
true => 'Skip',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->icons([
|
||||
false => 'tabler-code',
|
||||
true => 'tabler-code-off',
|
||||
])
|
||||
->required(),
|
||||
|
||||
Forms\Components\Textarea::make('startup')
|
||||
->hintIcon('tabler-code')
|
||||
->label('Startup Command')
|
||||
->required()
|
||||
->live()
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
0
|
||||
);
|
||||
}),
|
||||
|
||||
Forms\Components\Hidden::make('start_on_completion'),
|
||||
|
||||
Forms\Components\Section::make('Egg Variables')
|
||||
->icon('tabler-eggs')
|
||||
->iconColor('primary')
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->columnSpan(([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
]))
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
->relationship('serverVariables')
|
||||
->grid()
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||
foreach ($data as $key => $value) {
|
||||
if (!isset($data['variable_value'])) {
|
||||
$data['variable_value'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
})
|
||||
->reorderable(false)->addable(false)->deletable(false)
|
||||
->schema(function () {
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->maxLength(191)
|
||||
->rules([
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $serverVariable->variable->rules,
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
$select = Forms\Components\Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
||||
$components = [$text, $select];
|
||||
|
||||
/** @var Forms\Components\Component $component */
|
||||
foreach ($components as &$component) {
|
||||
$component = $component
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules)
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
|
||||
return $components;
|
||||
})
|
||||
->columnSpan(2),
|
||||
]),
|
||||
|
||||
Forms\Components\Section::make('Environment Management')
|
||||
->collapsed()
|
||||
->icon('tabler-server-cog')
|
||||
->iconColor('primary')
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 4,
|
||||
])
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make('Resource Limits')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk Space')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Space Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_cpu')
|
||||
->label('CPU')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||
->label('CPU Limit')->inlineLabel()
|
||||
->suffix('%')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('swap_support')
|
||||
->live()
|
||||
->label('Enable Swap Memory')->inlineLabel()->inline()
|
||||
->columnSpan(2)
|
||||
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||
$value = match ($state) {
|
||||
'unlimited' => -1,
|
||||
'disabled' => 0,
|
||||
'limited' => 128,
|
||||
};
|
||||
|
||||
$set('swap', $value);
|
||||
})
|
||||
->formatStateUsing(function (Forms\Get $get) {
|
||||
return match (true) {
|
||||
$get('swap') > 0 => 'limited',
|
||||
$get('swap') == 0 => 'disabled',
|
||||
$get('swap') < 0 => 'unlimited',
|
||||
};
|
||||
})
|
||||
->options([
|
||||
'unlimited' => 'Unlimited',
|
||||
'limited' => 'Limited',
|
||||
'disabled' => 'Disabled',
|
||||
])
|
||||
->colors([
|
||||
'unlimited' => 'primary',
|
||||
'limited' => 'warning',
|
||||
'disabled' => 'danger',
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('swap')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
|
||||
'disabled', 'unlimited', true => true,
|
||||
'limited', false => false,
|
||||
})
|
||||
->label('Swap Memory')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->minValue(-1)
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->integer(),
|
||||
]),
|
||||
|
||||
Forms\Components\Hidden::make('io')
|
||||
->helperText('The IO performance relative to other running containers')
|
||||
->label('Block IO Proportion'),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('oom_killer')
|
||||
->label('OOM Killer')->inlineLabel()->inline()
|
||||
->columnSpan(2)
|
||||
->options([
|
||||
false => 'Disabled',
|
||||
true => 'Enabled',
|
||||
])
|
||||
->colors([
|
||||
false => 'success',
|
||||
true => 'danger',
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('oom_disabled_hidden')
|
||||
->hidden(),
|
||||
]),
|
||||
]),
|
||||
|
||||
Forms\Components\Fieldset::make('Feature Limits')
|
||||
->inlineLabel()
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('allocation_limit')
|
||||
->suffixIcon('tabler-network')
|
||||
->required()
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('database_limit')
|
||||
->suffixIcon('tabler-database')
|
||||
->required()
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('backup_limit')
|
||||
->suffixIcon('tabler-copy-check')
|
||||
->required()
|
||||
->numeric(),
|
||||
]),
|
||||
Forms\Components\Fieldset::make('Docker Settings')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Select::make('select_image')
|
||||
->label('Image Name')
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
|
||||
->options(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
$currentImage = $get('image');
|
||||
if (!$currentImage && $images) {
|
||||
$defaultImage = collect($images)->first();
|
||||
$set('image', $defaultImage);
|
||||
$set('select_image', $defaultImage);
|
||||
}
|
||||
|
||||
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->columnSpan(1),
|
||||
|
||||
Forms\Components\TextInput::make('image')
|
||||
->label('Image')
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
if (in_array($state, $images)) {
|
||||
$set('select_image', $state);
|
||||
} else {
|
||||
$set('select_image', 'ghcr.io/custom-image');
|
||||
}
|
||||
})
|
||||
->placeholder('Enter a custom Image')
|
||||
->columnSpan(1),
|
||||
|
||||
Forms\Components\KeyValue::make('docker_labels')
|
||||
->label('Container Labels')
|
||||
->keyLabel('Label Name')
|
||||
->valueLabel('Label Description')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make('Delete')
|
||||
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||
->color('danger')
|
||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||
->requiresConfirmation(),
|
||||
Actions\Action::make('console')
|
||||
->label('Console')
|
||||
->icon('tabler-terminal')
|
||||
->url(fn (Server $server) => "/server/$server->uuid_short"),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
|
||||
}
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
unset($data['docker'], $data['status']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||
);
|
||||
|
||||
if ($component instanceof Forms\Components\Select) {
|
||||
return $containsRuleIn;
|
||||
}
|
||||
|
||||
if ($component instanceof Forms\Components\TextInput) {
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
|
||||
throw new \Exception('Component type not supported: ' . $component::class);
|
||||
}
|
||||
|
||||
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||
{
|
||||
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||
);
|
||||
|
||||
return str($inRule)
|
||||
->after('in:')
|
||||
->explode(',')
|
||||
->each(fn ($value) => str($value)->trim())
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Services\Exceptions\FilamentExceptionHandler;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use App\Models\User;
|
||||
@@ -66,7 +67,9 @@ class EditUser extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
Actions\DeleteAction::make()
|
||||
->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete'))
|
||||
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
@@ -75,4 +78,9 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function exception($exception, $stopPropagation): void
|
||||
{
|
||||
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class EggShareController extends Controller
|
||||
*/
|
||||
public function import(EggImportFormRequest $request): RedirectResponse
|
||||
{
|
||||
$egg = $this->importerService->handle($request->file('import_file'));
|
||||
$egg = $this->importerService->fromFile($request->file('import_file'));
|
||||
$this->alert->success(trans('admin/eggs.notices.imported'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', ['egg' => $egg->id]);
|
||||
@@ -61,7 +61,7 @@ class EggShareController extends Controller
|
||||
*/
|
||||
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
|
||||
{
|
||||
$this->updateImporterService->handle($egg, $request->file('import_file'));
|
||||
$this->updateImporterService->fromFile($egg, $request->file('import_file'));
|
||||
$this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', ['egg' => $egg]);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Nodes;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -13,7 +12,7 @@ class NodeController extends Controller
|
||||
/**
|
||||
* Returns a listing of nodes on the system.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$nodes = QueryBuilder::for(
|
||||
Node::query()->withCount('servers')
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Nodes;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\Allocation;
|
||||
@@ -29,16 +28,10 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Returns index view for a specific node on the system.
|
||||
*/
|
||||
public function index(Request $request, Node $node): View
|
||||
public function index(Node $node): View
|
||||
{
|
||||
$node->loadCount('servers');
|
||||
|
||||
$stats = Node::query()
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->join('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('node_id', '=', $node->id)
|
||||
->first();
|
||||
|
||||
return view('admin.nodes.view.index', [
|
||||
'node' => $node,
|
||||
'version' => $this->versionService,
|
||||
@@ -48,7 +41,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Returns the settings page for a specific node.
|
||||
*/
|
||||
public function settings(Request $request, Node $node): View
|
||||
public function settings(Node $node): View
|
||||
{
|
||||
return view('admin.nodes.view.settings', [
|
||||
'node' => $node,
|
||||
@@ -58,7 +51,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return the node configuration page for a specific node.
|
||||
*/
|
||||
public function configuration(Request $request, Node $node): View
|
||||
public function configuration(Node $node): View
|
||||
{
|
||||
return view('admin.nodes.view.configuration', compact('node'));
|
||||
}
|
||||
@@ -66,7 +59,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return the node allocation management page.
|
||||
*/
|
||||
public function allocations(Request $request, Node $node): View
|
||||
public function allocations(Node $node): View
|
||||
{
|
||||
$node->setRelation(
|
||||
'allocations',
|
||||
@@ -92,7 +85,7 @@ class NodeViewController extends Controller
|
||||
/**
|
||||
* Return a listing of servers that exist for this specific node.
|
||||
*/
|
||||
public function servers(Request $request, Node $node): View
|
||||
public function servers(Node $node): View
|
||||
{
|
||||
$this->plainInject([
|
||||
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Admin\Servers;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Server;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
@@ -16,7 +15,7 @@ class ServerController extends Controller
|
||||
* Returns all the servers that exist on the system using a paginated result set. If
|
||||
* a query is passed along in the request it is also passed to the repository function.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
|
||||
->allowedFilters([
|
||||
|
||||
@@ -37,7 +37,7 @@ class UserController extends Controller
|
||||
/**
|
||||
* Display user index page.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
public function index(): View
|
||||
{
|
||||
$users = QueryBuilder::for(
|
||||
User::query()->select('users.*')
|
||||
|
||||
@@ -33,10 +33,9 @@ class StoreNodeRequest extends ApplicationApiRequest
|
||||
'upload_size',
|
||||
'daemon_listen',
|
||||
'daemon_sftp',
|
||||
'daemon_sftp_alias',
|
||||
'daemon_base',
|
||||
])->mapWithKeys(function ($value, $key) {
|
||||
$key = ($key === 'daemon_sftp') ? 'daemon_sftp' : $key;
|
||||
|
||||
return [snake_case($key) => $value];
|
||||
})->toArray();
|
||||
}
|
||||
@@ -60,12 +59,8 @@ class StoreNodeRequest extends ApplicationApiRequest
|
||||
public function validated($key = null, $default = null): array
|
||||
{
|
||||
$response = parent::validated();
|
||||
$response['daemon_listen'] = $response['daemon_listen'];
|
||||
$response['daemon_sftp'] = $response['daemon_sftp'];
|
||||
$response['daemon_base'] = $response['daemon_base'] ?? (new Node())->getAttribute('daemon_base');
|
||||
|
||||
unset($response['daemon_base'], $response['daemon_listen'], $response['daemon_sftp']);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
* @property string $daemon_token
|
||||
* @property int $daemon_listen
|
||||
* @property int $daemon_sftp
|
||||
* @property string|null $daemon_sftp_alias
|
||||
* @property string $daemon_base
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
@@ -72,7 +73,7 @@ class Node extends Model
|
||||
'memory', 'memory_overallocate', 'disk',
|
||||
'disk_overallocate', 'cpu', 'cpu_overallocate',
|
||||
'upload_size', 'daemon_base',
|
||||
'daemon_sftp', 'daemon_listen',
|
||||
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen',
|
||||
'description', 'maintenance_mode',
|
||||
];
|
||||
|
||||
@@ -91,6 +92,7 @@ class Node extends Model
|
||||
'cpu_overallocate' => 'required|numeric|min:-1',
|
||||
'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
|
||||
'daemon_sftp' => 'required|numeric|between:1,65535',
|
||||
'daemon_sftp_alias' => 'nullable|string',
|
||||
'daemon_listen' => 'required|numeric|between:1,65535',
|
||||
'maintenance_mode' => 'boolean',
|
||||
'upload_size' => 'int|between:1,1024',
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Extensions\Themes\Theme;
|
||||
use App\Models;
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\Node;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\SecurityScheme;
|
||||
@@ -30,8 +31,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
View::share('appVersion', $this->versionData()['version'] ?? 'undefined');
|
||||
View::share('appIsGit', $this->versionData()['is_git'] ?? false);
|
||||
$versionData = app(SoftwareVersionService::class)->versionData();
|
||||
View::share('appVersion', $versionData['version'] ?? 'undefined');
|
||||
View::share('appIsGit', $versionData['is_git'] ?? false);
|
||||
|
||||
Paginator::useBootstrap();
|
||||
|
||||
@@ -96,34 +98,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
Scramble::ignoreDefaultRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return version information for the footer.
|
||||
*/
|
||||
protected function versionData(): array
|
||||
{
|
||||
return cache()->remember('git-version', 5, function () {
|
||||
if (file_exists(base_path('.git/HEAD'))) {
|
||||
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
|
||||
|
||||
if (array_key_exists(1, $head)) {
|
||||
$path = base_path('.git/' . trim($head[1]));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($path) && file_exists($path)) {
|
||||
return [
|
||||
'version' => substr(file_get_contents($path), 0, 8),
|
||||
'is_git' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => config('app.version'),
|
||||
'is_git' => false,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function bootAuth(): void
|
||||
{
|
||||
Sanctum::usePersonalAccessTokenModel(ApiKey::class);
|
||||
|
||||
@@ -124,38 +124,6 @@ class EggConfigurationService
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the legacy modifies from eggs with their new counterpart. The legacy Daemon would
|
||||
* set SERVER_MEMORY, SERVER_IP, and SERVER_PORT with their respective values on the Daemon
|
||||
* side. Ensure that anything referencing those properly replaces them with the matching config
|
||||
* value.
|
||||
*/
|
||||
protected function replaceLegacyModifiers(string $key, string $value): string
|
||||
{
|
||||
switch ($key) {
|
||||
case 'config.docker.interface':
|
||||
$replace = 'config.docker.network.interface';
|
||||
break;
|
||||
case 'server.build.env.SERVER_MEMORY':
|
||||
case 'env.SERVER_MEMORY':
|
||||
$replace = 'server.build.memory';
|
||||
break;
|
||||
case 'server.build.env.SERVER_IP':
|
||||
case 'env.SERVER_IP':
|
||||
$replace = 'server.build.default.ip';
|
||||
break;
|
||||
case 'server.build.env.SERVER_PORT':
|
||||
case 'env.SERVER_PORT':
|
||||
$replace = 'server.build.default.port';
|
||||
break;
|
||||
default:
|
||||
// By default, we don't need to change anything, only if we ended up matching a specific legacy item.
|
||||
$replace = $key;
|
||||
}
|
||||
|
||||
return str_replace("{{{$key}}}", "{{{$replace}}}", $value);
|
||||
}
|
||||
|
||||
protected function matchAndReplaceKeys(mixed $value, array $structure): mixed
|
||||
{
|
||||
preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches);
|
||||
@@ -175,8 +143,6 @@ class EggConfigurationService
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->replaceLegacyModifiers($key, $value);
|
||||
|
||||
// We don't want to do anything with config keys since the Daemon will need to handle
|
||||
// that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker
|
||||
// interface to proxy through, but the Panel would be unaware of that.
|
||||
@@ -198,7 +164,7 @@ class EggConfigurationService
|
||||
// variable from the server configuration.
|
||||
$plucked = Arr::get(
|
||||
$structure,
|
||||
preg_replace('/^env\./', 'build.env.', $key),
|
||||
preg_replace('/^env\./', 'build.environment.', $key),
|
||||
''
|
||||
);
|
||||
|
||||
|
||||
@@ -10,6 +10,16 @@ use App\Exceptions\Service\InvalidFileUploadException;
|
||||
|
||||
class EggParserService
|
||||
{
|
||||
public const UPGRADE_VARIABLES = [
|
||||
'server.build.env.SERVER_IP' => 'server.allocations.default.ip',
|
||||
'server.build.default.ip' => 'server.allocations.default.ip',
|
||||
'server.build.env.SERVER_PORT' => 'server.allocations.default.port',
|
||||
'server.build.default.port' => 'server.allocations.default.port',
|
||||
'server.build.env.SERVER_MEMORY' => 'server.build.memory_limit',
|
||||
'server.build.memory' => 'server.build.memory_limit',
|
||||
'server.build.env' => 'server.build.environment',
|
||||
];
|
||||
|
||||
/**
|
||||
* Takes an uploaded file and parses out the egg configuration from within.
|
||||
*
|
||||
@@ -26,11 +36,20 @@ class EggParserService
|
||||
|
||||
$version = $parsed['meta']['version'] ?? '';
|
||||
|
||||
return match ($version) {
|
||||
$parsed = match ($version) {
|
||||
'PTDL_v1' => $this->convertToV2($parsed),
|
||||
'PTDL_v2' => $parsed,
|
||||
default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.')
|
||||
};
|
||||
|
||||
// Make sure we only use recent variable format from now on
|
||||
$parsed['config']['files'] = str_replace(
|
||||
array_keys(self::UPGRADE_VARIABLES),
|
||||
array_values(self::UPGRADE_VARIABLES),
|
||||
$parsed['config']['files'] ?? '',
|
||||
);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Http\UploadedFile;
|
||||
use App\Models\EggVariable;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Services\Eggs\EggParserService;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
|
||||
class EggImporterService
|
||||
{
|
||||
@@ -21,7 +22,7 @@ class EggImporterService
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
*/
|
||||
public function handle(UploadedFile $file): Egg
|
||||
public function fromFile(UploadedFile $file): Egg
|
||||
{
|
||||
$parsed = $this->parser->handle($file);
|
||||
|
||||
@@ -45,4 +46,20 @@ class EggImporterService
|
||||
return $egg;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Take an url and parse it into a new egg.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
*/
|
||||
public function fromUrl(string $url): Egg
|
||||
{
|
||||
$info = pathinfo($url);
|
||||
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
|
||||
$tmpPath = $tmpDir->path($info['basename']);
|
||||
|
||||
file_put_contents($tmpPath, file_get_contents($url));
|
||||
|
||||
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Illuminate\Support\Collection;
|
||||
use App\Models\EggVariable;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Services\Eggs\EggParserService;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
|
||||
class EggUpdateImporterService
|
||||
{
|
||||
@@ -23,7 +24,7 @@ class EggUpdateImporterService
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
*/
|
||||
public function handle(Egg $egg, UploadedFile $file): Egg
|
||||
public function fromFile(Egg $egg, UploadedFile $file): Egg
|
||||
{
|
||||
$parsed = $this->parser->handle($file);
|
||||
|
||||
@@ -47,4 +48,20 @@ class EggUpdateImporterService
|
||||
return $egg->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing Egg using an url.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
*/
|
||||
public function fromUrl(Egg $egg, string $url): Egg
|
||||
{
|
||||
$info = pathinfo($url);
|
||||
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
|
||||
$tmpPath = $tmpDir->path($info['basename']);
|
||||
|
||||
file_put_contents($tmpPath, file_get_contents($url));
|
||||
|
||||
return $this->fromFile($egg, new UploadedFile($tmpPath, $info['basename'], 'application/json'));
|
||||
}
|
||||
}
|
||||
|
||||
24
app/Services/Exceptions/FilamentExceptionHandler.php
Normal file
24
app/Services/Exceptions/FilamentExceptionHandler.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class FilamentExceptionHandler
|
||||
{
|
||||
public function handle(Exception $exception, callable $stopPropagation): void
|
||||
{
|
||||
Notification::make()
|
||||
->title($exception->title ?? null)
|
||||
->body($exception->body ?? $exception->getMessage())
|
||||
->color($exception->color ?? 'danger')
|
||||
->icon($exception->icon ?? 'tabler-x')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
if ($this->stopPropagation ?? true) {
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,14 @@ class SoftwareVersionService
|
||||
return Arr::get(self::$result, 'discord') ?? 'https://pelican.dev/discord';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the donation URL.
|
||||
*/
|
||||
public function getDonations(): string
|
||||
{
|
||||
return Arr::get(self::$result, 'donate') ?? 'https://pelican.dev/donate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the current version of the panel is the latest.
|
||||
*/
|
||||
@@ -93,8 +101,28 @@ class SoftwareVersionService
|
||||
});
|
||||
}
|
||||
|
||||
public function getDonations(): string
|
||||
public function versionData(): array
|
||||
{
|
||||
return 'https://github.com';
|
||||
return cache()->remember('git-version', 5, function () {
|
||||
if (file_exists(base_path('.git/HEAD'))) {
|
||||
$head = explode(' ', file_get_contents(base_path('.git/HEAD')));
|
||||
|
||||
if (array_key_exists(1, $head)) {
|
||||
$path = base_path('.git/' . trim($head[1]));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($path) && file_exists($path)) {
|
||||
return [
|
||||
'version' => 'canary (' . substr(file_get_contents($path), 0, 8) . ')',
|
||||
'is_git' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'version' => config('app.version'),
|
||||
'is_git' => false,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ class NodeUpdateService
|
||||
*/
|
||||
public function handle(Node $node, array $data, bool $resetToken = false): Node
|
||||
{
|
||||
$data['id'] = $node->id;
|
||||
|
||||
if ($resetToken) {
|
||||
$data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH);
|
||||
$data['daemon_token_id'] = Str::random(Node::DAEMON_TOKEN_ID_LENGTH);
|
||||
@@ -35,15 +37,9 @@ class NodeUpdateService
|
||||
[$updated, $exception] = $this->connection->transaction(function () use ($data, $node) {
|
||||
/** @var \App\Models\Node $updated */
|
||||
$updated = $node->replicate();
|
||||
$updated->exists = true;
|
||||
$updated->forceFill($data)->save();
|
||||
try {
|
||||
// If we're changing the FQDN for the node, use the newly provided FQDN for the connection
|
||||
// address. This should alleviate issues where the node gets pointed to a "valid" FQDN that
|
||||
// isn't actually running the daemon software, and therefore you can't actually change it
|
||||
// back.
|
||||
//
|
||||
// This makes more sense anyways, because only the Panel uses the FQDN for connecting, the
|
||||
// node doesn't actually care about this.
|
||||
$node->fqdn = $updated->fqdn;
|
||||
|
||||
$this->configurationRepository->setNode($node)->update($updated);
|
||||
|
||||
@@ -46,6 +46,7 @@ class ServerTransformer extends BaseClientTransformer
|
||||
'is_node_under_maintenance' => $server->node->isUnderMaintenance(),
|
||||
'sftp_details' => [
|
||||
'ip' => $server->node->fqdn,
|
||||
'alias' => $server->node->daemon_sftp_alias,
|
||||
'port' => $server->node->daemon_sftp,
|
||||
],
|
||||
'description' => $server->description,
|
||||
|
||||
@@ -9,10 +9,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
\Prologue\Alerts\AlertsServiceProvider::class,
|
||||
])
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
// api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
// channels: __DIR__.'/../routes/channels.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"s1lentium/iptools": "~1.2.0",
|
||||
"spatie/laravel-fractal": "^6.2",
|
||||
"spatie/laravel-query-builder": "^5.8.1",
|
||||
"spatie/temporary-directory": "^2.2",
|
||||
"symfony/http-client": "^7.1",
|
||||
"symfony/mailgun-mailer": "^7.1",
|
||||
"symfony/postmark-mailer": "^7.0.7",
|
||||
|
||||
65
composer.lock
generated
65
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "8feeafbeb16044bd6716510a73393fc0",
|
||||
"content-hash": "bf44faee3aae2b1d4c1b57893c1aba98",
|
||||
"packages": [
|
||||
{
|
||||
"name": "abdelhamiderrahmouni/filament-monaco-editor",
|
||||
@@ -6990,6 +6990,67 @@
|
||||
],
|
||||
"time": "2024-05-10T08:19:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/temporary-directory",
|
||||
"version": "2.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spatie/temporary-directory.git",
|
||||
"reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/spatie/temporary-directory/zipball/76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a",
|
||||
"reference": "76949fa18f8e1a7f663fd2eaa1d00e0bcea0752a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Spatie\\TemporaryDirectory\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alex Vanderbist",
|
||||
"email": "alex@spatie.be",
|
||||
"homepage": "https://spatie.be",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Easily create, use and destroy temporary directories",
|
||||
"homepage": "https://github.com/spatie/temporary-directory",
|
||||
"keywords": [
|
||||
"php",
|
||||
"spatie",
|
||||
"temporary-directory"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/spatie/temporary-directory/issues",
|
||||
"source": "https://github.com/spatie/temporary-directory/tree/2.2.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://spatie.be/open-source/support-us",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/spatie",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-12-25T11:46:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v7.0.7",
|
||||
@@ -13096,5 +13157,5 @@
|
||||
"ext-zip": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.3.0"
|
||||
}
|
||||
|
||||
@@ -75,10 +75,10 @@ class EggSeeder extends Seeder
|
||||
->first();
|
||||
|
||||
if ($egg instanceof Egg) {
|
||||
$this->updateImporterService->handle($egg, $file);
|
||||
$this->updateImporterService->fromFile($egg, $file);
|
||||
$this->command->info('Updated ' . $decoded['name']);
|
||||
} else {
|
||||
$this->importerService->handle($file);
|
||||
$this->importerService->fromFile($file);
|
||||
$this->command->comment('Created ' . $decoded['name']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:01+00:00",
|
||||
"exported_at": "2024-06-04T22:51:49+00:00",
|
||||
"name": "Bungeecord",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "9e6b409e-4028-4947-aea8-50a2c404c271",
|
||||
@@ -24,7 +24,7 @@
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.build.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.build.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.allocations.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.allocations.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \"Listening on \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "end"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:02+00:00",
|
||||
"exported_at": "2024-06-04T22:51:58+00:00",
|
||||
"name": "Forge Minecraft",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "ed072427-f209-4603-875c-f540c6dd5a65",
|
||||
@@ -24,7 +24,7 @@
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:02+00:00",
|
||||
"exported_at": "2024-06-04T22:51:57+00:00",
|
||||
"name": "Paper",
|
||||
"author": "parker@example.com",
|
||||
"uuid": "5da37ef6-58da-4169-90a6-e683e1721247",
|
||||
@@ -24,7 +24,7 @@
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:03+00:00",
|
||||
"exported_at": "2024-06-04T22:50:55+00:00",
|
||||
"name": "Sponge (SpongeVanilla)",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
|
||||
@@ -24,7 +24,7 @@
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
@@ -60,4 +60,4 @@
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:03+00:00",
|
||||
"exported_at": "2024-06-04T22:51:16+00:00",
|
||||
"name": "Vanilla Minecraft",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b",
|
||||
@@ -24,7 +24,7 @@
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.build.default.port}}\",\r\n \"query.port\": \"{{server.build.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2024-06-02T20:42:08+00:00",
|
||||
"exported_at": "2024-06-04T22:53:03+00:00",
|
||||
"name": "Mumble Server",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "727ee758-7fb2-4979-972b-d3eba4e1e9f0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"file_denylist": [],
|
||||
"startup": "mumble-server -fg -ini murmur.ini",
|
||||
"config": {
|
||||
"files": "{\r\n \"murmur.ini\": {\r\n \"parser\": \"ini\",\r\n \"find\": {\r\n \"database\": \"\/home\/container\/murmur.sqlite\",\r\n \"logfile\": \"\/home\/container\/murmur.log\",\r\n \"port\": \"{{server.build.default.port}}\",\r\n \"host\": \"\",\r\n \"users\": \"{{server.build.env.MAX_USERS}}\"\r\n }\r\n }\r\n}",
|
||||
"files": "{\r\n \"murmur.ini\": {\r\n \"parser\": \"ini\",\r\n \"find\": {\r\n \"database\": \"\/home\/container\/murmur.sqlite\",\r\n \"logfile\": \"\/home\/container\/murmur.log\",\r\n \"port\": \"{{server.allocations.default.port}}\",\r\n \"host\": \"\",\r\n \"users\": \"{{server.environment.MAX_USERS}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \"Server listening on\"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "^C"
|
||||
|
||||
@@ -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('nodes', function (Blueprint $table) {
|
||||
$table->string('daemon_sftp_alias')->nullable()->after('daemon_sftp');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('nodes', function (Blueprint $table) {
|
||||
$table->dropColumn('daemon_sftp_alias');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$eggs = DB::table('eggs')->get();
|
||||
|
||||
foreach ($eggs as $egg) {
|
||||
$updatedPort = str_replace(
|
||||
'server.build.default.port',
|
||||
'server.allocations.default.port',
|
||||
$egg->config_files
|
||||
);
|
||||
|
||||
if ($updatedPort !== $egg->config_files) {
|
||||
$egg->config_files = $updatedPort;
|
||||
echo "Processed Port update with ID: {$egg->name}\n";
|
||||
}
|
||||
|
||||
$updatedIp = str_replace(
|
||||
'server.build.default.ip',
|
||||
'server.allocations.default.ip',
|
||||
$egg->config_files
|
||||
);
|
||||
|
||||
if ($updatedIp !== $egg->config_files) {
|
||||
$egg->config_files = $updatedIp;
|
||||
echo "Processed IP update with ID: {$egg->name}\n";
|
||||
}
|
||||
|
||||
$updatedEnv = str_replace(
|
||||
'server.build.env.',
|
||||
'server.environment.',
|
||||
$egg->config_files
|
||||
);
|
||||
|
||||
if ($updatedEnv !== $egg->config_files) {
|
||||
$egg->config_files = $updatedEnv;
|
||||
echo "Processed ENV update with ID: {$egg->name}\n";
|
||||
}
|
||||
|
||||
DB::table('eggs')
|
||||
->where('id', $egg->id)
|
||||
->update(['config_files' => $egg->config_files]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$eggs = DB::table('eggs')->get();
|
||||
|
||||
foreach ($eggs as $egg) {
|
||||
$revertedEnv = str_replace(
|
||||
'server.environment.',
|
||||
'server.build.env.',
|
||||
$egg->config_files
|
||||
);
|
||||
|
||||
if ($revertedEnv !== $egg->config_files) {
|
||||
$egg->config_files = $revertedEnv;
|
||||
}
|
||||
|
||||
$revertedIp = str_replace(
|
||||
'server.allocations.default.ip',
|
||||
'server.build.default.ip',
|
||||
$egg->config_files
|
||||
);
|
||||
|
||||
if ($revertedIp !== $egg->config_files) {
|
||||
$egg->config_files = $revertedIp;
|
||||
}
|
||||
|
||||
$revertedPort = str_replace(
|
||||
'server.allocations.default.port',
|
||||
'server.build.default.port',
|
||||
$egg->config_files
|
||||
);
|
||||
|
||||
if ($revertedPort !== $egg->config_files) {
|
||||
$egg->config_files = $revertedPort;
|
||||
}
|
||||
|
||||
DB::table('eggs')
|
||||
->where('id', $egg->id)
|
||||
->update(['config_files' => $egg->config_files]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -37,6 +37,7 @@ return [
|
||||
'upload_size' => "'Enter the maximum filesize upload",
|
||||
'daemonListen' => 'Enter the daemon listening port',
|
||||
'daemonSFTP' => 'Enter the daemon SFTP listening port',
|
||||
'daemonSFTPAlias' => 'Enter the daemon SFTP alias (can be empty)',
|
||||
'daemonBase' => 'Enter the base folder',
|
||||
'succes1' => 'Successfully created a new node with the name: ',
|
||||
'succes2' => 'and has an id of: ',
|
||||
|
||||
@@ -19,6 +19,10 @@ return [
|
||||
'button_issues' => 'Create Issue',
|
||||
'button_features' => 'Discuss Features',
|
||||
],
|
||||
'intro-update' => [
|
||||
'heading' => 'Update available',
|
||||
'content' => ':latestVersion is available! Read our documentation to update your Panel.',
|
||||
],
|
||||
'intro-first-node' => [
|
||||
'heading' => 'No Nodes Detected',
|
||||
'content' => "It looks like you don't have any Nodes set up yet, but don't worry because you click the action button to create your first one!",
|
||||
|
||||
58
readme.md
58
readme.md
@@ -26,46 +26,20 @@ This gives you the power to run game servers without bloating machines with a ho
|
||||
|
||||
Some of our popular eggs include but are not limited to:
|
||||
|
||||
* [Minecraft](https://github.com/pelican-eggs/minecraft)
|
||||
* Paper
|
||||
* Sponge
|
||||
* Bungeecord
|
||||
* Waterfall
|
||||
* [SteamCMD](https://github.com/pelican-eggs/steamcmd)
|
||||
* 7 Days to Die
|
||||
* ARK: Survival
|
||||
* ARMA
|
||||
* Counter Strike
|
||||
* DayZ
|
||||
* Enshrouded
|
||||
* Left 4 Dead
|
||||
* Palworld
|
||||
* Project Zomboid
|
||||
* Sons of the Forest
|
||||
* [Other Games](https://github.com/pelican-eggs/games)
|
||||
* Among Us
|
||||
* Factorio
|
||||
* GTA
|
||||
* Rimworld
|
||||
* Terraria
|
||||
* [Discord Bots](https://github.com/pelican-eggs/chatbots)
|
||||
* Redbot
|
||||
* JMusicBot
|
||||
* SinusBot
|
||||
* Dynamica
|
||||
* [Software](https://github.com/pelican-eggs/software)
|
||||
* [Programming Languages](https://github.com/pelican-eggs/generic)
|
||||
* C#
|
||||
* Java
|
||||
* Lua
|
||||
* Node.js
|
||||
* Python
|
||||
* [Database](https://github.com/pelican-eggs/database)
|
||||
* Redis
|
||||
* MariaDB
|
||||
* PostgreSQL
|
||||
* [Voice Servers](https://github.com/pelican-eggs/voice)
|
||||
* [Storage](https://github.com/pelican-eggs/storage)
|
||||
* [Monitoring](https://github.com/pelican-eggs/monitoring)
|
||||
| Category | Eggs | | | |
|
||||
|----------------------------------------------------------------------|-----------------|---------------|--------------------|----------------|
|
||||
| [Minecraft](https://github.com/pelican-eggs/minecraft) | Paper | Sponge | Bungeecord | Waterfall |
|
||||
| [SteamCMD](https://github.com/pelican-eggs/steamcmd) | 7 Days to Die | ARK: Survival | Arma 3 | Counter Strike |
|
||||
| | DayZ | Enshrouded | Left 4 Dead | Palworld |
|
||||
| | Project Zomboid | Satisfactory | Sons of the Forest | Starbound |
|
||||
| [Standalone Games](https://github.com/pelican-eggs/games-standalone) | Among Us | Factorio | FTL | GTA |
|
||||
| | Kerbal Space | Mindustry | Rimworld | Terraria |
|
||||
| [Discord Bots](https://github.com/pelican-eggs/chatbots) | Redbot | JMusicBot | JMusicBot | Dynamica |
|
||||
| [Voice Servers](https://github.com/pelican-eggs/voice) | Mumble | Teamspeak | Lavalink | |
|
||||
| [Software](https://github.com/pelican-eggs/software) | Elasticsearch | Gitea | Grafana | RabbitMQ |
|
||||
| [Programming](https://github.com/pelican-eggs/generic) | Node.js | Python | Java | C# |
|
||||
| [Databases](https://github.com/pelican-eggs/database) | Redis | MariaDB | PostgreSQL | MongoDB |
|
||||
| [Storage](https://github.com/pelican-eggs/storage) | S3 | SFTP Share | | |
|
||||
| [Monitoring](https://github.com/pelican-eggs/monitoring) | Prometheus | Loki | | |
|
||||
|
||||
Copyright Pelican® 2024
|
||||
*Copyright Pelican® 2024*
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Server {
|
||||
status: ServerStatus;
|
||||
sftpDetails: {
|
||||
ip: string;
|
||||
alias: string;
|
||||
port: number;
|
||||
};
|
||||
invocation: string;
|
||||
@@ -57,6 +58,7 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
|
||||
dockerImage: data.docker_image,
|
||||
sftpDetails: {
|
||||
ip: data.sftp_details.ip,
|
||||
alias: data.sftp_details.alias,
|
||||
port: data.sftp_details.port,
|
||||
},
|
||||
description: data.description ? (data.description.length > 0 ? data.description : null) : null,
|
||||
|
||||
@@ -40,7 +40,7 @@ const ModalContent = ({ ...props }: RequiredModalProps) => {
|
||||
label={'Ignored Files & Directories'}
|
||||
description={`
|
||||
Enter the files or folders to ignore while generating this backup. Leave blank to use
|
||||
the contents of the .panelignore file in the root of the server directory if present.
|
||||
the contents of the .pelicanignore file in the root of the server directory if present.
|
||||
Wildcard matching of files and folders is supported in addition to negating a rule by
|
||||
prefixing the path with an exclamation point.
|
||||
`}
|
||||
|
||||
@@ -91,13 +91,14 @@ export default () => {
|
||||
<FileManagerBreadcrumbs withinFileEditor isNewFile={action !== 'edit'} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
{hash.replace(/^#/, '').endsWith('.panelignore') && (
|
||||
{hash.replace(/^#/, '').endsWith('.pelicanignore') && (
|
||||
<div css={tw`mb-4 p-4 border-l-4 bg-neutral-900 rounded border-cyan-400`}>
|
||||
<p css={tw`text-neutral-300 text-sm`}>
|
||||
You're editing a <code css={tw`font-mono bg-black rounded py-px px-1`}>.panelignore</code>{' '}
|
||||
file. Any files or directories listed in here will be excluded from backups. Wildcards are
|
||||
supported by using an asterisk (<code css={tw`font-mono bg-black rounded py-px px-1`}>*</code>).
|
||||
You can negate a prior rule by prepending an exclamation point (
|
||||
You're editing a{' '}
|
||||
<code css={tw`font-mono bg-black rounded py-px px-1`}>.pelicanignore</code> directories listed
|
||||
in here will be excluded from backups. Wildcards are supported by using an supported by using an
|
||||
asterisk (<code css={tw`font-mono bg-black rounded py-px px-1`}>*</code>). You can negate a
|
||||
prior rule by prepending an exclamation point (
|
||||
<code css={tw`font-mono bg-black rounded py-px px-1`}>!</code>).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +168,7 @@ const TaskDetailsModal = ({ schedule, task }: Props) => {
|
||||
<FormikFieldWrapper
|
||||
name={'payload'}
|
||||
description={
|
||||
'Optional. Include the files and folders to be excluded in this backup. By default, the contents of your .panelignore file will be used. If you have reached your backup limit, the oldest backup will be rotated.'
|
||||
'Optional. Include the files and folders to be excluded in this backup. By default, the contents of your .pelicanignore file will be used. If you have reached your backup limit, the oldest backup will be rotated.'
|
||||
}
|
||||
>
|
||||
<FormikField as={Textarea} name={'payload'} rows={6} />
|
||||
|
||||
@@ -31,8 +31,12 @@ export default () => {
|
||||
<TitledGreyBox title={'SFTP Details'} css={tw`mb-6 md:mb-10`}>
|
||||
<div>
|
||||
<Label>Server Address</Label>
|
||||
<CopyOnClick text={`sftp://${ip(sftp.ip)}:${sftp.port}`}>
|
||||
<Input type={'text'} value={`sftp://${ip(sftp.ip)}:${sftp.port}`} readOnly />
|
||||
<CopyOnClick text={`sftp://${sftp.alias ? sftp.alias : ip(sftp.ip)}:${sftp.port}`}>
|
||||
<Input
|
||||
type={'text'}
|
||||
value={`sftp://${sftp.alias ? sftp.alias : ip(sftp.ip)}:${sftp.port}`}
|
||||
readOnly
|
||||
/>
|
||||
</CopyOnClick>
|
||||
</div>
|
||||
<div css={tw`mt-6`}>
|
||||
@@ -50,7 +54,10 @@ export default () => {
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`ml-4`}>
|
||||
<a href={`sftp://${username}.${id}@${ip(sftp.ip)}:${sftp.port}`}>
|
||||
<a
|
||||
href={`sftp://${username}.${id}@${sftp.alias ? sftp.alias : ip(sftp.ip)}:${sftp.port
|
||||
}`}
|
||||
>
|
||||
<Button.Text variant={Button.Variants.Secondary}>Launch SFTP</Button.Text>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:actions="$this->getCachedHeaderActions()"
|
||||
:breadcrumbs="filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : []"
|
||||
:heading=" trans('dashboard/index.heading')"
|
||||
:subheading="trans('strings.version', ['version' => config('app.version')])"
|
||||
:subheading="trans('strings.version', ['version' => $version])"
|
||||
></x-filament-panels::header>
|
||||
|
||||
<p>{{ trans('dashboard/index.expand_sections') }}</p>
|
||||
@@ -30,6 +30,22 @@
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if (!$isLatest)
|
||||
<x-filament::section
|
||||
icon="tabler-info-circle"
|
||||
icon-color="warning"
|
||||
id="intro-update"
|
||||
collapsible
|
||||
persist-collapsed
|
||||
:header-actions="$updateActions"
|
||||
>
|
||||
<x-slot name="heading">{{ trans('dashboard/index.sections.intro-update.heading') }}</x-slot>
|
||||
|
||||
<p>{{ trans('dashboard/index.sections.intro-update.content', ['latestVersion' => $latestVersion]) }}</p>
|
||||
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
{{-- No Nodes Created --}}
|
||||
@if ($nodesCount <= 0)
|
||||
<x-filament::section
|
||||
|
||||
Reference in New Issue
Block a user