mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
92 Commits
v1.0.0-bet
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73cefa59fe | ||
|
|
c18d291f8f | ||
|
|
45c0cfe4d8 | ||
|
|
89d555f308 | ||
|
|
9b2e00ead2 | ||
|
|
d0997dca1a | ||
|
|
cca723d21e | ||
|
|
68dbd6e329 | ||
|
|
544364f061 | ||
|
|
649f319776 | ||
|
|
6ffe800e0d | ||
|
|
12af8fe51e | ||
|
|
9f5d27896a | ||
|
|
15969da41e | ||
|
|
628a6e54e3 | ||
|
|
edcd406b82 | ||
|
|
1742061807 | ||
|
|
289eda64ad | ||
|
|
cc46c463ac | ||
|
|
e6dded61a4 | ||
|
|
921b76f1e1 | ||
|
|
91a3bb969e | ||
|
|
8b62df6c53 | ||
|
|
3c14e1ffa4 | ||
|
|
095fff7ad6 | ||
|
|
0cd03e95f4 | ||
|
|
00dda7dbe4 | ||
|
|
eee1260fcc | ||
|
|
71c91e086f | ||
|
|
94bbf46659 | ||
|
|
558b8b2ea7 | ||
|
|
4bb16887c4 | ||
|
|
0c72833af7 | ||
|
|
ee3f8cd3ec | ||
|
|
b513366f35 | ||
|
|
b1d056a301 | ||
|
|
c72d3f1338 | ||
|
|
7e6231e6de | ||
|
|
4ad837ff14 | ||
|
|
d7ebde5f5f | ||
|
|
6bdd1b3ccb | ||
|
|
afd9f2eb0e | ||
|
|
8a617d416e | ||
|
|
7f4cc64d11 | ||
|
|
38e117e304 | ||
|
|
4d272a1234 | ||
|
|
e9dc6cd32c | ||
|
|
98ba2c1b8b | ||
|
|
2d643ec79f | ||
|
|
2b4625e9b9 | ||
|
|
9f40ed2f84 | ||
|
|
af797b3018 | ||
|
|
e5146e0dbb | ||
|
|
586d1b413c | ||
|
|
7d239de7f6 | ||
|
|
5f13c15c70 | ||
|
|
4e3f919d8e | ||
|
|
7efa04f1ef | ||
|
|
35ac1f863a | ||
|
|
68195ab0b7 | ||
|
|
f79dac2d13 | ||
|
|
4f176c47d2 | ||
|
|
f7e7864dfe | ||
|
|
2f8a15facd | ||
|
|
7bf175190b | ||
|
|
8866ca3d96 | ||
|
|
703e5480ff | ||
|
|
45a6c37594 | ||
|
|
340085cc9b | ||
|
|
ef9d1ab614 | ||
|
|
55a0bfdf7e | ||
|
|
d6f3934f80 | ||
|
|
f9e8adad30 | ||
|
|
08dc5753d4 | ||
|
|
0c851ea075 | ||
|
|
cf3ea38e65 | ||
|
|
b813de0467 | ||
|
|
399bed7576 | ||
|
|
5aa7128b9c | ||
|
|
92d167eb10 | ||
|
|
f02eb5bfba | ||
|
|
f348ac9f0a | ||
|
|
893b2dca89 | ||
|
|
1f5217a9d9 | ||
|
|
e84b47410a | ||
|
|
76cf4391ae | ||
|
|
7688006574 | ||
|
|
ed36041a7e | ||
|
|
bb52485606 | ||
|
|
615b70f9a2 | ||
|
|
3609873c4c | ||
|
|
7f6e4a18c1 |
@@ -13,12 +13,8 @@ LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=sync
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=file
|
||||
|
||||
HASHIDS_SALT=
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: pelican-dev
|
||||
custom: [https://buy.stripe.com/14kdU99SI4UT7ni9AB, https://buy.stripe.com/14kaHXc0Q9b9372eUU]
|
||||
custom: [https://hub.pelican.dev/donors]
|
||||
|
||||
@@ -10,24 +10,23 @@ use Illuminate\Support\Facades\Artisan;
|
||||
class AppSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'redis' => 'Redis',
|
||||
'memcached' => 'Memcached',
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'redis' => 'Redis',
|
||||
'memcached' => 'Memcached',
|
||||
'database' => 'MySQL Database',
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'sync' => 'Synchronous (recommended)',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'MySQL Database',
|
||||
'sync' => 'Sync (recommended)',
|
||||
];
|
||||
|
||||
protected $description = 'Configure basic environment settings for the Panel.';
|
||||
@@ -86,7 +85,7 @@ class AppSettingsCommand extends Command
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('queue.default', 'sync');
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
|
||||
@@ -31,7 +31,7 @@ class EmailSettingsCommand extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$this->variables['MAIL_DRIVER'] = $this->option('driver') ?? $this->choice(
|
||||
$this->variables['MAIL_MAILER'] = $this->option('driver') ?? $this->choice(
|
||||
trans('command/messages.environment.mail.ask_driver'),
|
||||
[
|
||||
'log' => 'Log',
|
||||
@@ -41,10 +41,10 @@ class EmailSettingsCommand extends Command
|
||||
'mandrill' => 'Mandrill',
|
||||
'postmark' => 'Postmark',
|
||||
],
|
||||
'smtp',
|
||||
env('MAIL_MAILER', env('MAIL_DRIVER', 'smtp')),
|
||||
);
|
||||
|
||||
$method = 'setup' . studly_case($this->variables['MAIL_DRIVER']) . 'DriverVariables';
|
||||
$method = 'setup' . studly_case($this->variables['MAIL_MAILER']) . 'DriverVariables';
|
||||
if (method_exists($this, $method)) {
|
||||
$this->{$method}();
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
|
||||
@@ -12,9 +12,13 @@ class ApiKeyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ApiKey::class;
|
||||
protected static ?string $label = 'API Key';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-key';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function canEdit($record): bool
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -14,6 +14,11 @@ class DatabaseHostResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -22,50 +22,59 @@ class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live()
|
||||
->debounce(500)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->required(),
|
||||
Forms\Components\Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
]),
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = encrypt($data['password']);
|
||||
@@ -73,4 +82,17 @@ class CreateDatabaseHost extends CreateRecord
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getCreateFormAction()->formId('form'),
|
||||
];
|
||||
|
||||
}
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,45 +17,54 @@ class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live()
|
||||
->debounce(500)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->required(),
|
||||
Forms\Components\Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -63,6 +72,7 @@ class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -74,4 +84,16 @@ class EditDatabaseHost extends EditRecord
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
DatabaseHostResource\RelationManagers\DatabasesRelationManager::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ class ListDatabaseHosts extends ListRecords
|
||||
{
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected ?string $heading = 'Database Hosts';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
@@ -48,7 +50,7 @@ class ListDatabaseHosts extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
Actions\CreateAction::make('create')->label('New Database Host'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
|
||||
|
||||
use App\Models\Database;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class DatabasesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'databases';
|
||||
|
||||
protected $listeners = ['refresh' => 'refreshForm'];
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('database')->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('username'),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->hintAction(
|
||||
Action::make('rotate')
|
||||
->icon('tabler-refresh')
|
||||
->requiresConfirmation()
|
||||
->action(fn (DatabasePasswordService $service, Database $database) => $service->handle($database))
|
||||
)
|
||||
->formatStateUsing(fn (Database $database) => decrypt($database->password)),
|
||||
Forms\Components\TextInput::make('remote')->label('Connections From'),
|
||||
Forms\Components\TextInput::make('max_connections'),
|
||||
Forms\Components\TextInput::make('JDBC')
|
||||
->label('JDBC Connection String')
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn (Forms\Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode(decrypt($database->password)) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
|
||||
]);
|
||||
}
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('database')->icon('tabler-database'),
|
||||
Tables\Columns\TextColumn::make('username')->icon('tabler-user'),
|
||||
//Tables\Columns\TextColumn::make('password'),
|
||||
Tables\Columns\TextColumn::make('remote'),
|
||||
Tables\Columns\TextColumn::make('server.name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
|
||||
Tables\Columns\TextColumn::make('max_connections'),
|
||||
Tables\Columns\TextColumn::make('created_at')->dateTime(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
Tables\Actions\ViewAction::make()->color('primary'),
|
||||
//Tables\Actions\EditAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@ class DatabaseResource extends Resource
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -16,6 +16,11 @@ class EggResource extends Resource
|
||||
|
||||
protected static ?string $recordRouteKeyName = 'id';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -31,6 +31,7 @@ class CreateEgg extends CreateRecord
|
||||
Forms\Components\TextInput::make('author')
|
||||
->maxLength(191)
|
||||
->required()
|
||||
->email()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('The author of this version of the Egg.'),
|
||||
Forms\Components\Textarea::make('description')
|
||||
|
||||
@@ -25,12 +25,16 @@ class EditEgg extends EditRecord
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1])
|
||||
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||
Forms\Components\TextInput::make('uuid')
|
||||
->label('Egg UUID')
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'),
|
||||
Forms\Components\TextInput::make('id')
|
||||
->label('Egg ID')
|
||||
->disabled(),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->rows(3)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
@@ -38,6 +42,7 @@ class EditEgg extends EditRecord
|
||||
Forms\Components\TextInput::make('author')
|
||||
->required()
|
||||
->maxLength(191)
|
||||
->email()
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'),
|
||||
@@ -202,6 +207,19 @@ class EditEgg extends EditRecord
|
||||
->color('primary')
|
||||
// TODO uses old admin panel export service
|
||||
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
EggResource\RelationManagers\ServersRelationManager::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\EggResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ServersRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'servers';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
|
||||
->searchable(false)
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('user.username')
|
||||
->label('Owner')
|
||||
->icon('tabler-user')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('node.name')
|
||||
->icon('tabler-server-2')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
|
||||
Tables\Columns\TextColumn::make('image')
|
||||
->label('Docker Image'),
|
||||
Tables\Columns\SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ class MountResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Filament\Resources\MountResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MountResource;
|
||||
use App\Services\Servers\ServerCreationService;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
|
||||
@@ -98,6 +98,12 @@ class EditMount extends EditRecord
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ class NodeResource extends Resource
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
@@ -17,179 +18,302 @@ class CreateNode extends CreateRecord
|
||||
|
||||
public function form(Forms\Form $form): Forms\Form
|
||||
{
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return '
|
||||
return $form->schema([
|
||||
Tabs::make('Tabs')
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return '
|
||||
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
|
||||
You must use a domain name, because you cannot get SSL certificates for IP Addresses
|
||||
';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return "
|
||||
return "
|
||||
This is the domain name that points to your node's IP Address.
|
||||
If you've already set up this, you can verify it by checking the next field!
|
||||
";
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return 'You cannot connect to an IP Address over SSL';
|
||||
}
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return 'You cannot connect to an IP Address over SSL';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', collect($validRecords)->first());
|
||||
$set('ip', collect($validRecords)->first());
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
|
||||
Forms\Components\TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
Forms\Components\TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('dns')
|
||||
->label('DNS Record Check')
|
||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
false => 'Invalid',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Forms\Components\ToggleButtons::make('dns')
|
||||
->label('DNS Record Check')
|
||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
false => 'Invalid',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(0)
|
||||
->maxValue(65536)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
Forms\Components\TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(0)
|
||||
->maxValue(65536)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
Forms\Components\ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->inline()
|
||||
->helperText(function (Forms\Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
Forms\Components\ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->inline()
|
||||
->helperText(function (Forms\Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return 'An IP address cannot use SSL.';
|
||||
}
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return 'An IP address cannot use SSL.';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('strings.description')
|
||||
->hidden()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->rows(5),
|
||||
|
||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
||||
]);
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
]),
|
||||
Tabs\Tab::make('Advanced Settings')
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('upload_size')
|
||||
->label('Upload Limit')
|
||||
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->default(256)
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix('MiB'),
|
||||
Forms\Components\ToggleButtons::make('public')
|
||||
->label('Automatic Allocation')->inline()
|
||||
->default(true)
|
||||
->columnSpan(1)
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
Forms\Components\ToggleButtons::make('maintenance_mode')
|
||||
->label('Maintenance Mode')->inline()
|
||||
->columnSpan(1)
|
||||
->default(false)
|
||||
->hinticon('tabler-question-mark')
|
||||
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||
->options([
|
||||
true => 'Enable',
|
||||
false => 'Disable',
|
||||
])
|
||||
->colors([
|
||||
true => 'danger',
|
||||
false => 'success',
|
||||
]),
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
->label('Tags')
|
||||
->disabled()
|
||||
->placeholder('Not Implemented')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Not Implemented')
|
||||
->columnSpan(1),
|
||||
Forms\Components\Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 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')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
Forms\Components\TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%'),
|
||||
]),
|
||||
Forms\Components\Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 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 Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
Forms\Components\TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getRedirectUrlParameters(): array
|
||||
|
||||
@@ -32,13 +32,293 @@ class EditNode extends EditRecord
|
||||
->tabs([
|
||||
Tabs\Tab::make('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->schema((new CreateNode())->form($form)->getComponents()),
|
||||
// Tabs\Tab::make('Advanced Settings')
|
||||
// ->icon('tabler-server-cog')
|
||||
// ->schema([
|
||||
// Forms\Components\Placeholder::make('Coming soon!'),
|
||||
// ]),
|
||||
Tabs\Tab::make('Configuration')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return '
|
||||
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
|
||||
You must use a domain name, because you cannot get SSL certificates for IP Addresses
|
||||
';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return "
|
||||
This is the domain name that points to your node's IP Address.
|
||||
If you've already set up this, you can verify it by checking the next field!
|
||||
";
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return 'You cannot connect to an IP Address over SSL';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', collect($validRecords)->first());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
|
||||
Forms\Components\TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('dns')
|
||||
->label('DNS Record Check')
|
||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
false => 'Invalid',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(0)
|
||||
->maxValue(65536)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
Forms\Components\ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->inline()
|
||||
->helperText(function (Forms\Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return 'An IP address cannot use SSL.';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||
Tabs\Tab::make('Advanced Settings')
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('id')
|
||||
->label('Node ID')
|
||||
->disabled(),
|
||||
Forms\Components\TextInput::make('uuid')
|
||||
->label('Node UUID')
|
||||
->hintAction(CopyAction::make())
|
||||
->columnSpan(2)
|
||||
->disabled(),
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
->label('Tags')
|
||||
->disabled()
|
||||
->placeholder('Not Implemented')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Not Implemented')
|
||||
->columnSpan(1),
|
||||
Forms\Components\ToggleButtons::make('public')
|
||||
->label('Automatic Allocation')->inline()
|
||||
->columnSpan(1)
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
Forms\Components\ToggleButtons::make('maintenance_mode')
|
||||
->label('Maintenance Mode')->inline()
|
||||
->columnSpan(1)
|
||||
->hinticon('tabler-question-mark')
|
||||
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||
->options([
|
||||
true => 'Enable',
|
||||
false => 'Disable',
|
||||
])
|
||||
->colors([
|
||||
true => 'danger',
|
||||
false => 'success',
|
||||
]),
|
||||
Forms\Components\TextInput::make('upload_size')
|
||||
->label('Upload Limit')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->columnStart(4)->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix('MiB'),
|
||||
Forms\Components\Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 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\TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->required()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
Forms\Components\Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 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 Limit')->inlineLabel()
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
Forms\Components\TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Configuration File')
|
||||
->icon('tabler-code')
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('instructions')
|
||||
@@ -66,18 +346,17 @@ class EditNode extends EditRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getSteps(): array
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [
|
||||
];
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->disabled(fn (Node $node) => $node->servers()->count() > 0)
|
||||
->label(fn (Node $node) => $node->servers()->count() > 0 ? 'Node Has Servers' : 'Delete'),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -42,15 +42,15 @@ class ListNodes extends ListRecords
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-device-desktop-analytics')
|
||||
->numeric()
|
||||
->suffix(' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
|
||||
->suffix(' GiB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('disk')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-file')
|
||||
->numeric()
|
||||
->suffix(' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
|
||||
->suffix(' GiB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('scheme')
|
||||
->visibleFrom('xl')
|
||||
|
||||
@@ -43,10 +43,13 @@ class AllocationsRelationManager extends RelationManager
|
||||
Tables\Columns\TextColumn::make('server.name')
|
||||
->label('Server')
|
||||
->icon('tabler-brand-docker')
|
||||
->searchable()
|
||||
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
|
||||
Tables\Columns\TextColumn::make('ip_alias')
|
||||
Tables\Columns\TextInputColumn::make('ip_alias')
|
||||
->searchable()
|
||||
->label('Alias'),
|
||||
Tables\Columns\TextColumn::make('ip')
|
||||
Tables\Columns\TextInputColumn::make('ip')
|
||||
->searchable()
|
||||
->label('IP'),
|
||||
Tables\Columns\TextColumn::make('port')
|
||||
->searchable()
|
||||
@@ -126,6 +129,8 @@ class AllocationsRelationManager extends RelationManager
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ class NodeMemoryChart extends ChartWidget
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$total = $node->statistics()['memory_total'] ?? 0;
|
||||
$used = $node->statistics()['memory_used'] ?? 0;
|
||||
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$unused = $total - $used;
|
||||
|
||||
return [
|
||||
|
||||
@@ -40,8 +40,8 @@ class NodeStorageChart extends ChartWidget
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$total = $node->statistics()['disk_total'] ?? 0;
|
||||
$used = $node->statistics()['disk_used'] ?? 0;
|
||||
$total = ($node->statistics()['disk_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$used = ($node->statistics()['disk_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$unused = $total - $used;
|
||||
|
||||
return [
|
||||
|
||||
@@ -14,6 +14,11 @@ class ServerResource extends Resource
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -487,11 +487,12 @@ class CreateServer extends CreateRecord
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->default(0)
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@@ -517,11 +518,12 @@ class CreateServer extends CreateRecord
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Space Limit')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->default(0)
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@@ -551,7 +553,9 @@ class CreateServer extends CreateRecord
|
||||
->default(0)
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->helperText('100% equals one logical thread'),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@@ -593,7 +597,7 @@ class CreateServer extends CreateRecord
|
||||
})
|
||||
->label('Swap Memory')
|
||||
->default(0)
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->minValue(-1)
|
||||
->columnSpan(2)
|
||||
->inlineLabel()
|
||||
@@ -610,7 +614,7 @@ class CreateServer extends CreateRecord
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('oom_disabled')
|
||||
Forms\Components\ToggleButtons::make('oom_killer')
|
||||
->label('OOM Killer')
|
||||
->inlineLabel()->inline()
|
||||
->default(false)
|
||||
|
||||
@@ -134,7 +134,7 @@ class EditServer extends EditRecord
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 6,
|
||||
'lg' => 5,
|
||||
])
|
||||
->relationship('egg', 'name')
|
||||
->searchable()
|
||||
@@ -157,59 +157,48 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->required(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('custom_image')
|
||||
->live()
|
||||
->label('Custom Image?')->inline()
|
||||
->formatStateUsing(function ($state, Forms\Get $get) {
|
||||
if ($state !== null) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$images = Egg::find($get('egg_id'))->docker_images;
|
||||
|
||||
return !in_array($get('image'), $images);
|
||||
})
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Yes',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->icons([
|
||||
false => 'tabler-settings-cancel',
|
||||
true => 'tabler-settings-check',
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('image')
|
||||
->hidden(fn (Forms\Get $get) => !$get('custom_image'))
|
||||
->disabled(fn (Forms\Get $get) => !$get('custom_image'))
|
||||
->label('Docker Image')
|
||||
->placeholder('Enter a custom Image')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->required(),
|
||||
|
||||
Forms\Components\Select::make('image')
|
||||
->hidden(fn (Forms\Get $get) => $get('custom_image'))
|
||||
->disabled(fn (Forms\Get $get) => $get('custom_image'))
|
||||
->label('Docker Image')
|
||||
Forms\Components\Select::make('select_image')
|
||||
->label('Docker Image Name')
|
||||
->prefixIcon('tabler-brand-docker')
|
||||
->options(fn (Forms\Get $get) => Egg::find($get('egg_id'))->docker_images)
|
||||
->disabled(fn (Forms\Components\Select $component) => empty($component->getOptions()))
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('image'))
|
||||
->options(function ($state, Forms\Get $get) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
return ['ghcr.io/custom-image' => 'Custom Image'] + array_flip($images);
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->required(),
|
||||
'lg' => 3,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('image')
|
||||
->label('Docker Image')
|
||||
->prefixIcon('tabler-brand-docker')
|
||||
->live()
|
||||
->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([
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
]),
|
||||
|
||||
Forms\Components\Textarea::make('startup')
|
||||
->hintIcon('tabler-code')
|
||||
@@ -244,35 +233,57 @@ class EditServer extends EditRecord
|
||||
]))
|
||||
->schema([
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
->label('')
|
||||
->relationship('serverVariables')
|
||||
->grid()
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('variable_value')
|
||||
->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 $variable): Closure => function (string $attribute, $value, Closure $fail) use ($variable) {
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $variable->variable->rules,
|
||||
'validatorkey' => $serverVariable->variable->rules,
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $variable->variable->name);
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
])
|
||||
->label(fn (ServerVariable $variable) => $variable->variable->name)
|
||||
->hintIcon('tabler-code')
|
||||
->hintIconTooltip(fn (ServerVariable $variable) => $variable->variable->rules)
|
||||
->prefix(fn (ServerVariable $variable) => '{{' . $variable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $variable) => $variable->variable->description ?: '—')
|
||||
->maxLength(191),
|
||||
]);
|
||||
|
||||
Forms\Components\Hidden::make('variable_id'),
|
||||
])
|
||||
$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),
|
||||
]),
|
||||
|
||||
@@ -311,10 +322,11 @@ class EditServer extends EditRecord
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@@ -340,10 +352,11 @@ class EditServer extends EditRecord
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Space Limit')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@@ -372,7 +385,8 @@ class EditServer extends EditRecord
|
||||
->suffix('%')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric(),
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
@@ -417,7 +431,7 @@ class EditServer extends EditRecord
|
||||
'limited', false => false,
|
||||
})
|
||||
->label('Swap Memory')->inlineLabel()
|
||||
->suffix('MB')
|
||||
->suffix('MiB')
|
||||
->minValue(-1)
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
@@ -432,7 +446,7 @@ class EditServer extends EditRecord
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('oom_disabled')
|
||||
Forms\Components\ToggleButtons::make('oom_killer')
|
||||
->label('OOM Killer')->inlineLabel()->inline()
|
||||
->columnSpan(2)
|
||||
->options([
|
||||
@@ -487,13 +501,6 @@ class EditServer extends EditRecord
|
||||
->color('danger')
|
||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||
->requiresConfirmation(),
|
||||
Actions\DeleteAction::make('Force Delete')
|
||||
->label('Force Delete')
|
||||
->hidden()
|
||||
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||
->color('danger')
|
||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->withForce()->handle($server))
|
||||
->requiresConfirmation(),
|
||||
Actions\Action::make('console')
|
||||
->label('Console')
|
||||
->icon('tabler-terminal')
|
||||
@@ -520,4 +527,35 @@ class EditServer extends EditRecord
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
// ->actions
|
||||
// ->groups
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('ip_alias')->label('Alias'),
|
||||
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
|
||||
Tables\Columns\TextColumn::make('ip')->label('IP'),
|
||||
Tables\Columns\TextColumn::make('port')->label('Port'),
|
||||
Tables\Columns\IconColumn::make('primary')
|
||||
@@ -56,8 +56,8 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||
//Tables\Actions\AssociateAction::make()->label('Add Allocation'),
|
||||
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||
//TODO Tables\Actions\AssociateAction::make()->label('Add Allocation'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
|
||||
@@ -15,6 +15,11 @@ class UserResource extends Resource
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'username';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
@@ -26,7 +31,6 @@ class UserResource extends Resource
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
'create' => Pages\CreateUser::route('/create'),
|
||||
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use App\Models\User;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Form;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class CreateUser extends CreateRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('username')->required()->maxLength(191),
|
||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(191),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||
->dehydrated(fn (?string $state): bool => filled($state))
|
||||
->required(fn (string $operation): bool => $operation === 'create')
|
||||
->password(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('root_admin')
|
||||
->label('Administrator (Root)')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Admin',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->disableOptionWhen(function (string $operation, $value, User $user) {
|
||||
if ($operation !== 'edit' || $value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->isLastRootAdmin();
|
||||
})
|
||||
->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '')
|
||||
->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '')
|
||||
->hintColor('warning')
|
||||
->inline()
|
||||
->required()
|
||||
->default(false),
|
||||
|
||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
||||
Forms\Components\Select::make('language')
|
||||
->required()
|
||||
->hidden()
|
||||
->default('en')
|
||||
->options(fn (User $user) => $user->getAvailableLanguages()),
|
||||
])->columns(2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,12 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use Filament\Actions;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
use Filament\Forms;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
@@ -73,8 +76,49 @@ class ListUsers extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->label('Create User'),
|
||||
Actions\CreateAction::make('create')
|
||||
->label('Create User')
|
||||
->createAnother(false)
|
||||
->form([
|
||||
Forms\Components\Grid::make()
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('username')
|
||||
->alphaNum()
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('email')
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(191),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||
->password(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('root_admin')
|
||||
->label('Administrator (Root)')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Admin',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->inline()
|
||||
->required()
|
||||
->default(false),
|
||||
]),
|
||||
])
|
||||
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
||||
->action(function (array $data) {
|
||||
resolve(UserCreationService::class)->handle($data);
|
||||
Notification::make()->title('User Created!')->success()->send();
|
||||
|
||||
return redirect()->route('filament.admin.resources.users.index');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,12 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Servers;
|
||||
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Node;
|
||||
use Carbon\CarbonImmutable;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Lcobucci\JWT\Token\Plain;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use App\Models\ServerTransfer;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
|
||||
class ServerTransferController extends Controller
|
||||
{
|
||||
@@ -25,30 +16,10 @@ class ServerTransferController extends Controller
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private ConnectionInterface $connection,
|
||||
private NodeJWTService $nodeJWTService,
|
||||
private TransferServerService $transferServerService,
|
||||
) {
|
||||
}
|
||||
|
||||
private function notify(Server $server, Plain $token): void
|
||||
{
|
||||
try {
|
||||
Http::daemon($server->node)->post('/api/transfer', [
|
||||
'json' => [
|
||||
'server_id' => $server->uuid,
|
||||
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
|
||||
'token' => 'Bearer ' . $token->toString(),
|
||||
'server' => [
|
||||
'uuid' => $server->uuid,
|
||||
'start_on_completion' => false,
|
||||
],
|
||||
],
|
||||
])->toPsrResponse();
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a transfer of a server to a new node.
|
||||
*
|
||||
@@ -62,85 +33,12 @@ class ServerTransferController extends Controller
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
$node_id = $validatedData['node_id'];
|
||||
$allocation_id = intval($validatedData['allocation_id']);
|
||||
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
|
||||
|
||||
// Check if the node is viable for the transfer.
|
||||
$node = Node::query()
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('nodes.id', $node_id)
|
||||
->first();
|
||||
|
||||
if (!$node->isViable($server->memory, $server->disk)) {
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
|
||||
} else {
|
||||
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
}
|
||||
|
||||
$server->validateTransferState();
|
||||
|
||||
$this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) {
|
||||
// Create a new ServerTransfer entry.
|
||||
$transfer = new ServerTransfer();
|
||||
|
||||
$transfer->server_id = $server->id;
|
||||
$transfer->old_node = $server->node_id;
|
||||
$transfer->new_node = $node_id;
|
||||
$transfer->old_allocation = $server->allocation_id;
|
||||
$transfer->new_allocation = $allocation_id;
|
||||
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id')->all();
|
||||
$transfer->new_additional_allocations = $additional_allocations;
|
||||
|
||||
$transfer->save();
|
||||
|
||||
// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
|
||||
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
|
||||
|
||||
// Generate a token for the destination node that the source node can use to authenticate with.
|
||||
$token = $this->nodeJWTService
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setSubject($server->uuid)
|
||||
->handle($transfer->newNode, $server->uuid, 'sha256');
|
||||
|
||||
// Notify the source node of the pending outgoing transfer.
|
||||
$this->notify($server, $token);
|
||||
|
||||
return $transfer;
|
||||
});
|
||||
|
||||
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
|
||||
|
||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the specified allocations to the specified server.
|
||||
*/
|
||||
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
|
||||
{
|
||||
$allocations = $additional_allocations;
|
||||
$allocations[] = $allocation_id;
|
||||
|
||||
$node = Node::query()->findOrFail($node_id);
|
||||
$unassigned = $node->allocations()
|
||||
->whereNull('server_id')
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
$updateIds = [];
|
||||
foreach ($allocations as $allocation) {
|
||||
if (!in_array($allocation, $unassigned)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$updateIds[] = $allocation;
|
||||
}
|
||||
|
||||
if (!empty($updateIds)) {
|
||||
Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class ServersController extends Controller
|
||||
$this->buildModificationService->handle($server, $request->only([
|
||||
'allocation_id', 'add_allocations', 'remove_allocations',
|
||||
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
|
||||
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
|
||||
'database_limit', 'allocation_limit', 'backup_limit', 'oom_killer',
|
||||
]));
|
||||
} catch (DataValidationException $exception) {
|
||||
throw new ValidationException($exception->getValidator());
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\DatabaseHosts;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Models\DatabaseHost;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Services\Databases\Hosts\HostUpdateService;
|
||||
use App\Services\Databases\Hosts\HostCreationService;
|
||||
use App\Transformers\Api\Application\DatabaseHostTransformer;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\DatabaseHosts\GetDatabaseHostRequest;
|
||||
use App\Http\Requests\Api\Application\DatabaseHosts\StoreDatabaseHostRequest;
|
||||
use App\Http\Requests\Api\Application\DatabaseHosts\DeleteDatabaseHostRequest;
|
||||
use App\Http\Requests\Api\Application\DatabaseHosts\UpdateDatabaseHostRequest;
|
||||
|
||||
class DatabaseHostController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* DatabaseHostController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private HostCreationService $creationService,
|
||||
private HostUpdateService $updateService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the database hosts currently registered on the Panel.
|
||||
*/
|
||||
public function index(GetDatabaseHostRequest $request): array
|
||||
{
|
||||
$databases = QueryBuilder::for(DatabaseHost::query())
|
||||
->allowedFilters(['name', 'host'])
|
||||
->allowedSorts(['id', 'name', 'host'])
|
||||
->paginate($request->query('per_page') ?? 10);
|
||||
|
||||
return $this->fractal->collection($databases)
|
||||
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single database host.
|
||||
*/
|
||||
public function view(GetDatabaseHostRequest $request, DatabaseHost $databaseHost): array
|
||||
{
|
||||
return $this->fractal->item($databaseHost)
|
||||
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new database host on the Panel and return an HTTP/201 response code with the
|
||||
* new database host attached.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(StoreDatabaseHostRequest $request): JsonResponse
|
||||
{
|
||||
$databaseHost = $this->creationService->handle($request->validated());
|
||||
|
||||
return $this->fractal->item($databaseHost)
|
||||
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
|
||||
->addMeta([
|
||||
'resource' => route('api.application.databases.view', [
|
||||
'database_host' => $databaseHost->id,
|
||||
]),
|
||||
])
|
||||
->respond(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a database host on the Panel and return the updated record to the user.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function update(UpdateDatabaseHostRequest $request, DatabaseHost $databaseHost): array
|
||||
{
|
||||
$databaseHost = $this->updateService->handle($databaseHost->id, $request->validated());
|
||||
|
||||
return $this->fractal->item($databaseHost)
|
||||
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a database host from the Panel.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete(DeleteDatabaseHostRequest $request, DatabaseHost $databaseHost): Response
|
||||
{
|
||||
$databaseHost->delete();
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Servers;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class ServerManagementController extends ApplicationApiController
|
||||
{
|
||||
@@ -16,7 +18,9 @@ class ServerManagementController extends ApplicationApiController
|
||||
*/
|
||||
public function __construct(
|
||||
private ReinstallServerService $reinstallServerService,
|
||||
private SuspensionService $suspensionService
|
||||
private SuspensionService $suspensionService,
|
||||
private TransferServerService $transferServerService,
|
||||
private DaemonServerRepository $daemonServerRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -57,4 +61,44 @@ class ServerManagementController extends ApplicationApiController
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a transfer of a server to a new node.
|
||||
*/
|
||||
public function startTransfer(ServerWriteRequest $request, Server $server): Response
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
// Transfer started
|
||||
$this->returnNoContent();
|
||||
} else {
|
||||
// Node was not viable
|
||||
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a transfer of a server to a new node.
|
||||
*
|
||||
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function cancelTransfer(ServerWriteRequest $request, Server $server): Response
|
||||
{
|
||||
if (!$transfer = $server->transfer) {
|
||||
// Server is not transferring
|
||||
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
|
||||
}
|
||||
|
||||
$transfer->successful = true;
|
||||
$transfer->save();
|
||||
|
||||
$this->daemonServerRepository->setServer($server)->cancelTransfer();
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Extensions\Backups\BackupManager;
|
||||
use App\Extensions\Filesystem\S3Filesystem;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use App\Exceptions\Http\HttpForbiddenException;
|
||||
|
||||
class BackupRemoteUploadController extends Controller
|
||||
{
|
||||
@@ -32,18 +33,32 @@ class BackupRemoteUploadController extends Controller
|
||||
*/
|
||||
public function __invoke(Request $request, string $backup): JsonResponse
|
||||
{
|
||||
// Get the node associated with the request.
|
||||
/** @var \App\Models\Node $node */
|
||||
$node = $request->attributes->get('node');
|
||||
|
||||
// Get the size query parameter.
|
||||
$size = (int) $request->query('size');
|
||||
if (empty($size)) {
|
||||
throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.');
|
||||
}
|
||||
|
||||
/** @var \App\Models\Backup $backup */
|
||||
$backup = Backup::query()->where('uuid', $backup)->firstOrFail();
|
||||
/** @var \App\Models\Backup $model */
|
||||
$model = Backup::query()
|
||||
->where('uuid', $backup)
|
||||
->firstOrFail();
|
||||
|
||||
// Check that the backup is "owned" by the node making the request. This avoids other nodes
|
||||
// from messing with backups that they don't own.
|
||||
/** @var \App\Models\Server $server */
|
||||
$server = $model->server;
|
||||
if ($server->node_id !== $node->id) {
|
||||
throw new HttpForbiddenException('You do not have permission to access that backup.');
|
||||
}
|
||||
|
||||
// Prevent backups that have already been completed from trying to
|
||||
// be uploaded again.
|
||||
if (!is_null($backup->completed_at)) {
|
||||
if (!is_null($model->completed_at)) {
|
||||
throw new ConflictHttpException('This backup is already in a completed state.');
|
||||
}
|
||||
|
||||
@@ -54,7 +69,7 @@ class BackupRemoteUploadController extends Controller
|
||||
}
|
||||
|
||||
// The path where backup will be uploaded to
|
||||
$path = sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid);
|
||||
$path = sprintf('%s/%s.tar.gz', $model->server->uuid, $model->uuid);
|
||||
|
||||
// Get the S3 client
|
||||
$client = $adapter->getClient();
|
||||
@@ -92,7 +107,7 @@ class BackupRemoteUploadController extends Controller
|
||||
}
|
||||
|
||||
// Set the upload_id on the backup in the database.
|
||||
$backup->update(['upload_id' => $params['UploadId']]);
|
||||
$model->update(['upload_id' => $params['UploadId']]);
|
||||
|
||||
return new JsonResponse([
|
||||
'parts' => $parts,
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Extensions\Backups\BackupManager;
|
||||
use App\Extensions\Filesystem\S3Filesystem;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use App\Http\Requests\Api\Remote\ReportBackupCompleteRequest;
|
||||
use App\Exceptions\Http\HttpForbiddenException;
|
||||
|
||||
class BackupStatusController extends Controller
|
||||
{
|
||||
@@ -30,8 +31,22 @@ class BackupStatusController extends Controller
|
||||
*/
|
||||
public function index(ReportBackupCompleteRequest $request, string $backup): JsonResponse
|
||||
{
|
||||
// Get the node associated with the request.
|
||||
/** @var \App\Models\Node $node */
|
||||
$node = $request->attributes->get('node');
|
||||
|
||||
/** @var \App\Models\Backup $model */
|
||||
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
|
||||
$model = Backup::query()
|
||||
->where('uuid', $backup)
|
||||
->firstOrFail();
|
||||
|
||||
// Check that the backup is "owned" by the node making the request. This avoids other nodes
|
||||
// from messing with backups that they don't own.
|
||||
/** @var \App\Models\Server $server */
|
||||
$server = $model->server;
|
||||
if ($server->node_id !== $node->id) {
|
||||
throw new HttpForbiddenException('You do not have permission to access that backup.');
|
||||
}
|
||||
|
||||
if ($model->is_successful) {
|
||||
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\DatabaseHosts;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class DeleteDatabaseHostRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\DatabaseHosts;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class GetDatabaseHostRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
|
||||
|
||||
protected int $permission = AdminAcl::READ;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\DatabaseHosts;
|
||||
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class StoreDatabaseHostRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_DATABASE_HOSTS;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
|
||||
public function rules(array $rules = null): array
|
||||
{
|
||||
return $rules ?? DatabaseHost::getRules();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\DatabaseHosts;
|
||||
|
||||
use App\Models\DatabaseHost;
|
||||
|
||||
class UpdateDatabaseHostRequest extends StoreDatabaseHostRequest
|
||||
{
|
||||
public function rules(array $rules = null): array
|
||||
{
|
||||
/** @var DatabaseHost $databaseHost */
|
||||
$databaseHost = $this->route()->parameter('database_host');
|
||||
|
||||
return $rules ?? DatabaseHost::getRulesForUpdate($databaseHost->id);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
'startup' => $rules['startup'],
|
||||
'environment' => 'present|array',
|
||||
'skip_scripts' => 'sometimes|boolean',
|
||||
'oom_disabled' => 'sometimes|boolean',
|
||||
'oom_killer' => 'sometimes|boolean',
|
||||
|
||||
// Resource limitations
|
||||
'limits' => 'required|array',
|
||||
@@ -94,7 +94,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
'database_limit' => array_get($data, 'feature_limits.databases'),
|
||||
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
||||
'backup_limit' => array_get($data, 'feature_limits.backups'),
|
||||
'oom_disabled' => array_get($data, 'oom_disabled'),
|
||||
'oom_killer' => array_get($data, 'oom_killer'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
||||
|
||||
return [
|
||||
'allocation' => $rules['allocation_id'],
|
||||
'oom_disabled' => $rules['oom_disabled'],
|
||||
'oom_killer' => $rules['oom_killer'],
|
||||
|
||||
'limits' => 'sometimes|array',
|
||||
'limits.memory' => $this->requiredToOptional('memory', $rules['memory'], true),
|
||||
|
||||
@@ -19,9 +19,11 @@ class StoreScheduleRequest extends ViewScheduleRequest
|
||||
return [
|
||||
'name' => $rules['name'],
|
||||
'is_active' => array_merge(['filled'], $rules['is_active']),
|
||||
'only_when_online' => $rules['only_when_online'],
|
||||
'minute' => $rules['cron_minute'],
|
||||
'hour' => $rules['cron_hour'],
|
||||
'day_of_month' => $rules['cron_day_of_month'],
|
||||
'month' => $rules['cron_month'],
|
||||
'day_of_week' => $rules['cron_day_of_week'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @property int $io
|
||||
* @property int $cpu
|
||||
* @property string|null $threads
|
||||
* @property bool $oom_disabled
|
||||
* @property bool $oom_killer
|
||||
* @property int $allocation_id
|
||||
* @property int $egg_id
|
||||
* @property string $startup
|
||||
@@ -90,7 +90,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereMemory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereNodeId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomDisabled($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomKiller($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOwnerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value)
|
||||
@@ -124,7 +124,7 @@ class Server extends Model
|
||||
*/
|
||||
protected $attributes = [
|
||||
'status' => ServerState::Installing,
|
||||
'oom_disabled' => true,
|
||||
'oom_killer' => false,
|
||||
'installed_at' => null,
|
||||
];
|
||||
|
||||
@@ -150,7 +150,7 @@ class Server extends Model
|
||||
'io' => 'required|numeric|between:0,1000',
|
||||
'cpu' => 'required|numeric|min:0',
|
||||
'threads' => 'nullable|regex:/^[0-9-,]+$/',
|
||||
'oom_disabled' => 'sometimes|boolean',
|
||||
'oom_killer' => 'sometimes|boolean',
|
||||
'disk' => 'required|numeric|min:0',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'egg_id' => 'required|exists:eggs,id',
|
||||
@@ -174,7 +174,7 @@ class Server extends Model
|
||||
'disk' => 'integer',
|
||||
'io' => 'integer',
|
||||
'cpu' => 'integer',
|
||||
'oom_disabled' => 'boolean',
|
||||
'oom_killer' => 'boolean',
|
||||
'allocation_id' => 'integer',
|
||||
'egg_id' => 'integer',
|
||||
'database_limit' => 'integer',
|
||||
@@ -326,6 +326,11 @@ class Server extends Model
|
||||
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
|
||||
}
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return 'id';
|
||||
}
|
||||
|
||||
public function resolveRouteBinding($value, $field = null): ?self
|
||||
{
|
||||
return match ($field) {
|
||||
|
||||
@@ -6,6 +6,9 @@ use App\Extensions\Themes\Theme;
|
||||
use App\Models;
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\Node;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\SecurityScheme;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
@@ -66,6 +69,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
|
||||
$this->bootAuth();
|
||||
$this->bootBroadcast();
|
||||
|
||||
Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']]);
|
||||
Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']]);
|
||||
Scramble::registerApi('remote', ['api_path' => 'api/remote', 'info' => ['version' => '1.0']]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,6 +88,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
$this->app->singleton('extensions.themes', function () {
|
||||
return new Theme();
|
||||
});
|
||||
|
||||
Scramble::extendOpenApi(fn (OpenApi $openApi) => $openApi->secure(SecurityScheme::http('bearer')));
|
||||
Scramble::ignoreDefaultRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,7 @@ class AdminPanelProvider extends PanelProvider
|
||||
public function boot()
|
||||
{
|
||||
FilamentAsset::registerCssVariables([
|
||||
'sidebar-width' => '12rem !important',
|
||||
'sidebar-width' => '14rem !important',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -35,10 +35,11 @@ class AdminPanelProvider extends PanelProvider
|
||||
->default()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->topNavigation(config('panel.filament.top-navigation', false))
|
||||
->login()
|
||||
->brandName('Pelican')
|
||||
->homeUrl('/')
|
||||
->favicon('/pelican.ico')
|
||||
->brandName('Pelican')
|
||||
->profile(EditProfile::class, false)
|
||||
->colors([
|
||||
'danger' => Color::Red,
|
||||
|
||||
@@ -141,6 +141,46 @@ class DaemonServerRepository extends DaemonRepository
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a server transfer.
|
||||
*
|
||||
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
|
||||
*/
|
||||
public function cancelTransfer(): void
|
||||
{
|
||||
Assert::isInstanceOf($this->server, Server::class);
|
||||
|
||||
if ($transfer = $this->server->transfer) {
|
||||
// Source node
|
||||
$this->setNode($transfer->oldNode);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->delete(sprintf(
|
||||
'/api/servers/%s/transfer',
|
||||
$this->server->uuid
|
||||
));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
// Destination node
|
||||
$this->setNode($transfer->newNode);
|
||||
|
||||
try {
|
||||
$this->getHttpClient()->delete('/api/transfer', [
|
||||
'json' => [
|
||||
'server_id' => $this->server->uuid,
|
||||
'server' => [
|
||||
'uuid' => $this->server->uuid,
|
||||
],
|
||||
],
|
||||
]);
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes a single user's JTI by using their ID. This is simply a helper function to
|
||||
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted
|
||||
|
||||
@@ -40,8 +40,12 @@ class BuildModificationService
|
||||
throw_unless($existingAllocation, new DisplayException('The requested default allocation is not currently assigned to this server.'));
|
||||
}
|
||||
|
||||
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
|
||||
$data['oom_killer'] = !$data['oom_disabled'];
|
||||
}
|
||||
|
||||
// If any of these values are passed through in the data array go ahead and set them correctly on the server model.
|
||||
$merge = Arr::only($data, ['oom_disabled', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
|
||||
$merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
|
||||
|
||||
$server->forceFill(array_merge($merge, [
|
||||
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
|
||||
|
||||
@@ -59,14 +59,12 @@ class ServerConfigurationStructureService
|
||||
'cpu_limit' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'disk_space' => $server->disk,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
// This field is deprecated — use "oom_killer".
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'oom_killer' => $server->oom_killer,
|
||||
],
|
||||
'container' => [
|
||||
'image' => $server->image,
|
||||
// This field is deprecated — use the value in the "build" block.
|
||||
//
|
||||
// TODO: remove this key in V2.
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
'requires_rebuild' => false,
|
||||
],
|
||||
'allocations' => [
|
||||
@@ -110,7 +108,7 @@ class ServerConfigurationStructureService
|
||||
return $item->pluck('port');
|
||||
})->toArray(),
|
||||
'env' => $this->environment->handle($server),
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'memory' => (int) $server->memory,
|
||||
'swap' => (int) $server->swap,
|
||||
'io' => (int) $server->io,
|
||||
|
||||
@@ -47,6 +47,10 @@ class ServerCreationService
|
||||
*/
|
||||
public function handle(array $data, DeploymentObject $deployment = null): Server
|
||||
{
|
||||
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
|
||||
$data['oom_killer'] = !$data['oom_disabled'];
|
||||
}
|
||||
|
||||
// If a deployment object has been passed we need to get the allocation
|
||||
// that the server should use, and assign the node from that allocation.
|
||||
if ($deployment instanceof DeploymentObject) {
|
||||
@@ -142,7 +146,7 @@ class ServerCreationService
|
||||
'io' => Arr::get($data, 'io'),
|
||||
'cpu' => Arr::get($data, 'cpu'),
|
||||
'threads' => Arr::get($data, 'threads'),
|
||||
'oom_disabled' => Arr::get($data, 'oom_disabled') ?? true,
|
||||
'oom_killer' => Arr::get($data, 'oom_killer') ?? false,
|
||||
'allocation_id' => Arr::get($data, 'allocation_id'),
|
||||
'egg_id' => Arr::get($data, 'egg_id'),
|
||||
'startup' => Arr::get($data, 'startup'),
|
||||
|
||||
131
app/Services/Servers/TransferServerService.php
Normal file
131
app/Services/Servers/TransferServerService.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Servers;
|
||||
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerTransfer;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use Carbon\CarbonImmutable;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Lcobucci\JWT\Token\Plain;
|
||||
|
||||
class TransferServerService
|
||||
{
|
||||
/**
|
||||
* TransferService constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private ConnectionInterface $connection,
|
||||
private NodeJWTService $nodeJWTService,
|
||||
) {
|
||||
}
|
||||
|
||||
private function notify(Server $server, Plain $token): void
|
||||
{
|
||||
try {
|
||||
Http::daemon($server->node)->post('/api/transfer', [
|
||||
'json' => [
|
||||
'server_id' => $server->uuid,
|
||||
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
|
||||
'token' => 'Bearer ' . $token->toString(),
|
||||
'server' => [
|
||||
'uuid' => $server->uuid,
|
||||
'start_on_completion' => false,
|
||||
],
|
||||
],
|
||||
])->toPsrResponse();
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a transfer of a server to a new node.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(Server $server, array $data): bool
|
||||
{
|
||||
$node_id = $data['node_id'];
|
||||
$allocation_id = intval($data['allocation_id']);
|
||||
$additional_allocations = array_map('intval', $data['allocation_additional'] ?? []);
|
||||
|
||||
// Check if the node is viable for the transfer.
|
||||
$node = Node::query()
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
|
||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
||||
->where('nodes.id', $node_id)
|
||||
->first();
|
||||
|
||||
if (!$node->isViable($server->memory, $server->disk)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$server->validateTransferState();
|
||||
|
||||
$this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) {
|
||||
// Create a new ServerTransfer entry.
|
||||
$transfer = new ServerTransfer();
|
||||
|
||||
$transfer->server_id = $server->id;
|
||||
$transfer->old_node = $server->node_id;
|
||||
$transfer->new_node = $node_id;
|
||||
$transfer->old_allocation = $server->allocation_id;
|
||||
$transfer->new_allocation = $allocation_id;
|
||||
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id')->all();
|
||||
$transfer->new_additional_allocations = $additional_allocations;
|
||||
|
||||
$transfer->save();
|
||||
|
||||
// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
|
||||
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
|
||||
|
||||
// Generate a token for the destination node that the source node can use to authenticate with.
|
||||
$token = $this->nodeJWTService
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setSubject($server->uuid)
|
||||
->handle($transfer->newNode, $server->uuid, 'sha256');
|
||||
|
||||
// Notify the source node of the pending outgoing transfer.
|
||||
$this->notify($server, $token);
|
||||
|
||||
return $transfer;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the specified allocations to the specified server.
|
||||
*/
|
||||
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
|
||||
{
|
||||
$allocations = $additional_allocations;
|
||||
$allocations[] = $allocation_id;
|
||||
|
||||
$node = Node::query()->findOrFail($node_id);
|
||||
$unassigned = $node->allocations()
|
||||
->whereNull('server_id')
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
$updateIds = [];
|
||||
foreach ($allocations as $allocation) {
|
||||
if (!in_array($allocation, $unassigned)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$updateIds[] = $allocation;
|
||||
}
|
||||
|
||||
if (!empty($updateIds)) {
|
||||
Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use App\Models\Node;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use League\Fractal\Resource\Item;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
@@ -12,6 +14,7 @@ class DatabaseHostTransformer extends BaseTransformer
|
||||
{
|
||||
protected array $availableIncludes = [
|
||||
'databases',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -54,4 +57,20 @@ class DatabaseHostTransformer extends BaseTransformer
|
||||
|
||||
return $this->collection($model->getRelation('databases'), $this->makeTransformer(ServerDatabaseTransformer::class), Database::RESOURCE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the node associated with this host.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeNode(DatabaseHost $model): Item|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$model->loadMissing('node');
|
||||
|
||||
return $this->item($model->getRelation('node'), $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,9 @@ class ServerTransformer extends BaseTransformer
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
// This field is deprecated, please use "oom_killer".
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'oom_killer' => $server->oom_killer,
|
||||
],
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
|
||||
@@ -56,7 +56,9 @@ class ServerTransformer extends BaseClientTransformer
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_disabled' => $server->oom_disabled,
|
||||
// This field is deprecated, please use "oom_killer".
|
||||
'oom_disabled' => !$server->oom_killer,
|
||||
'oom_killer' => $server->oom_killer,
|
||||
],
|
||||
'invocation' => $service->handle($server, !$user->can(Permission::ACTION_STARTUP_READ, $server)),
|
||||
'docker_image' => $server->image,
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_mysql": "*",
|
||||
"ext-zip": "*",
|
||||
"abdelhamiderrahmouni/filament-monaco-editor": "^0.2.0",
|
||||
"abdelhamiderrahmouni/filament-monaco-editor": "0.2.1",
|
||||
"aws/aws-sdk-php": "~3.288.1",
|
||||
"chillerlan/php-qrcode": "^5.0",
|
||||
"dedoc/scramble": "^0.9.0",
|
||||
"chillerlan/php-qrcode": "^5.0.2",
|
||||
"dedoc/scramble": "^0.10.0",
|
||||
"doctrine/dbal": "~3.6.0",
|
||||
"filament/filament": "^3.2",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"guzzlehttp/guzzle": "^7.8.1",
|
||||
"hashids/hashids": "~5.0.0",
|
||||
"laracasts/utilities": "~3.2.2",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/framework": "^11.7",
|
||||
"laravel/helpers": "^1.7",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/sanctum": "^4.0.2",
|
||||
"laravel/tinker": "^2.9",
|
||||
"laravel/ui": "^4.4",
|
||||
"laravel/ui": "^4.5.1",
|
||||
"lcobucci/jwt": "~4.3.0",
|
||||
"league/flysystem-aws-s3-v3": "~3.12.2",
|
||||
"league/flysystem-memory": "~3.10.3",
|
||||
@@ -32,25 +32,25 @@
|
||||
"prologue/alerts": "^1.2",
|
||||
"ryangjchandler/blade-tabler-icons": "^2.3",
|
||||
"s1lentium/iptools": "~1.2.0",
|
||||
"spatie/laravel-fractal": "^6.1",
|
||||
"spatie/laravel-query-builder": "^5.8",
|
||||
"symfony/mailgun-mailer": "^7.0",
|
||||
"symfony/postmark-mailer": "^7.0",
|
||||
"symfony/yaml": "^7.0",
|
||||
"webbingbrasil/filament-copyactions": "^3.0",
|
||||
"spatie/laravel-fractal": "^6.2",
|
||||
"spatie/laravel-query-builder": "^5.8.1",
|
||||
"symfony/mailgun-mailer": "^7.0.7",
|
||||
"symfony/postmark-mailer": "^7.0.7",
|
||||
"symfony/yaml": "^7.0.7",
|
||||
"webbingbrasil/filament-copyactions": "^3.0.1",
|
||||
"webmozart/assert": "~1.11.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^3.0",
|
||||
"fakerphp/faker": "^1.23",
|
||||
"fakerphp/faker": "^1.23.1",
|
||||
"itsgoingd/clockwork": "~5.1.12",
|
||||
"larastan/larastan": "^2.8",
|
||||
"laravel/pint": "^1.14",
|
||||
"laravel/sail": "^1.26",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"spatie/laravel-ignition": "^2.4"
|
||||
"larastan/larastan": "^2.9.6",
|
||||
"laravel/pint": "^1.15.3",
|
||||
"laravel/sail": "^1.29.1",
|
||||
"mockery/mockery": "^1.6.11",
|
||||
"nunomaduro/collision": "^8.1.1",
|
||||
"phpunit/phpunit": "^10.5.20",
|
||||
"spatie/laravel-ignition": "^2.7"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
||||
709
composer.lock
generated
709
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,9 @@ use Illuminate\Support\Facades\Facade;
|
||||
|
||||
return [
|
||||
|
||||
'name' => 'Pelican',
|
||||
'name' => env('APP_NAME', 'Pelican'),
|
||||
|
||||
'version' => 'canary',
|
||||
'version' => '1.0.0-beta2',
|
||||
|
||||
'exceptions' => [
|
||||
'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false),
|
||||
|
||||
@@ -46,6 +46,8 @@ return [
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'client' => env('REDIS_CLIENT', 'predis'),
|
||||
|
||||
'default' => [
|
||||
'scheme' => env('REDIS_SCHEME', 'tcp'),
|
||||
'path' => env('REDIS_PATH', '/run/redis/redis.sock'),
|
||||
|
||||
@@ -163,4 +163,16 @@ return [
|
||||
// Should an email be sent to a server owner whenever their server is reinstalled?
|
||||
'send_reinstall_notification' => env('PANEL_SEND_REINSTALL_NOTIFICATION', true),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| FilamentPHP Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This section controls Filament configurations
|
||||
*/
|
||||
|
||||
'filament' => [
|
||||
'top-navigation' => env('FILAMENT_TOP_NAVIGATION', false),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -35,7 +35,7 @@ class ServerFactory extends Factory
|
||||
'io' => 500,
|
||||
'cpu' => 0,
|
||||
'threads' => null,
|
||||
'oom_disabled' => 0,
|
||||
'oom_killer' => false,
|
||||
'startup' => '/bin/bash echo "hello world"',
|
||||
'image' => 'foo/bar:latest',
|
||||
'allocation_limit' => null,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->tinyInteger('oom_killer')->unsigned()->default(0)->after('oom_disabled');
|
||||
});
|
||||
|
||||
DB::table('servers')->select(['id', 'oom_disabled'])->cursor()->each(function ($server) {
|
||||
DB::table('servers')->where('id', $server->id)->update(['oom_killer' => !$server->oom_disabled]);
|
||||
});
|
||||
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('oom_disabled');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->tinyInteger('oom_disabled')->unsigned()->default(0)->after('oom_killer');
|
||||
});
|
||||
|
||||
DB::table('servers')->select(['id', 'oom_killer'])->cursor()->each(function ($server) {
|
||||
DB::table('servers')->where('id', $server->id)->update(['oom_disabled' => !$server->oom_killer]);
|
||||
});
|
||||
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropColumn('oom_killer');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -495,7 +495,7 @@ CREATE TABLE `servers` (
|
||||
`io` int unsigned NOT NULL,
|
||||
`cpu` int unsigned NOT NULL,
|
||||
`threads` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`oom_disabled` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`oom_killer` tinyint unsigned NOT NULL DEFAULT '0',
|
||||
`allocation_id` int unsigned NOT NULL,
|
||||
`egg_id` int unsigned NOT NULL,
|
||||
`startup` text COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
@@ -844,3 +844,4 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (197,'2024_03_12_15
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (198,'2024_03_14_055537_remove_locations_table',2);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (201,'2024_04_20_214441_add_egg_var_sort',3);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (203,'2024_04_14_002250_update_column_names',4);
|
||||
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (204,'2024_05_08_094823_rename_oom_disabled_column_to_oom_killer',1);
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="MAIL_DRIVER" value="array"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||

|
||||
<img width="20%" src="https://raw.githubusercontent.com/pelican-dev/panel/main/public/pelican.svg" alt="logo">
|
||||
|
||||
# Pelican Panel
|
||||
|
||||
|
||||
@@ -203,8 +203,8 @@
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<div class="checkbox checkbox-primary no-margin-bottom">
|
||||
<input type="checkbox" id="pOomDisabled" name="oom_disabled" value="0" {{ \App\Helpers\Utilities::checked('oom_disabled', 0) }} />
|
||||
<label for="pOomDisabled" class="strong">Enable OOM Killer</label>
|
||||
<input type="checkbox" id="pOomKiller" name="oom_killer" value="0" {{ \App\Helpers\Utilities::checked('oom_killer', 0) }} />
|
||||
<label for="pOomKiller" class="strong">Enable OOM Killer</label>
|
||||
</div>
|
||||
|
||||
<p class="small text-muted no-margin">Terminates the server if it breaches the memory limits. Enabling OOM killer may cause server processes to exit unexpectedly.</p>
|
||||
|
||||
@@ -74,11 +74,11 @@
|
||||
<label for="cpu" class="control-label">OOM Killer</label>
|
||||
<div>
|
||||
<div class="radio radio-danger radio-inline">
|
||||
<input type="radio" id="pOomKillerEnabled" value="0" name="oom_disabled" @if(!$server->oom_disabled)checked @endif>
|
||||
<input type="radio" id="pOomKillerEnabled" value="1" name="oom_killer" @if(!$server->oom_killer)checked @endif>
|
||||
<label for="pOomKillerEnabled">Enabled</label>
|
||||
</div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pOomKillerDisabled" value="1" name="oom_disabled" @if($server->oom_disabled)checked @endif>
|
||||
<input type="radio" id="pOomKillerDisabled" value="0" name="oom_killer" @if($server->oom_killer)checked @endif>
|
||||
<label for="pOomKillerDisabled">Disabled</label>
|
||||
</div>
|
||||
<p class="text-muted small">
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
<x-filament-panels::page>
|
||||
|
||||
<x-filament::tabs disabled>
|
||||
<x-filament::tabs.item disabled>{{ trans('dashboard/index.overview') }} </x-filament::tabs.item>
|
||||
|
||||
<x-filament::tabs.item
|
||||
icon="tabler-server-2"
|
||||
>
|
||||
Nodes
|
||||
<x-slot name="badge">{{ $nodesCount }}</x-slot>
|
||||
</x-filament::tabs.item>
|
||||
|
||||
<x-filament::tabs.item
|
||||
icon="tabler-brand-docker"
|
||||
>
|
||||
Servers
|
||||
<x-slot name="badge">{{ $serversCount }}</x-slot>
|
||||
</x-filament::tabs.item>
|
||||
|
||||
<x-filament::tabs.item
|
||||
icon="tabler-eggs"
|
||||
>
|
||||
Eggs
|
||||
<x-slot name="badge">{{ $eggsCount }}</x-slot>
|
||||
</x-filament::tabs.item>
|
||||
|
||||
<x-filament::tabs.item
|
||||
icon="tabler-users"
|
||||
>
|
||||
Users
|
||||
<x-slot name="badge">{{ $usersCount }}</x-slot>
|
||||
</x-filament::tabs.item>
|
||||
</x-filament::tabs>
|
||||
|
||||
<x-filament-panels::header
|
||||
:actions="$this->getCachedHeaderActions()"
|
||||
:breadcrumbs="filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : []"
|
||||
|
||||
20
resources/views/vendor/scramble/docs.blade.php
vendored
20
resources/views/vendor/scramble/docs.blade.php
vendored
@@ -1,20 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="{{ config('scramble.theme', 'dark') }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Pelican - {{ str($api ?? 'all')->title() }} API Docs</title>
|
||||
|
||||
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
|
||||
</head>
|
||||
<body style="height: 100vh; overflow-y: hidden">
|
||||
<elements-api
|
||||
apiDescriptionUrl="{{ route('scramble.docs.' . $api ?? 'all') }}"
|
||||
tryItCredentialsPolicy="{{ config('scramble.ui.try_it_credentials_policy', 'include') }}"
|
||||
router="hash"
|
||||
@if(config('scramble.ui.hide_try_it')) hideTryIt="true" @endif
|
||||
logo="/pelican.svg"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
@@ -69,6 +69,8 @@ Route::prefix('/servers')->group(function () {
|
||||
Route::post('/{server:id}/suspend', [Application\Servers\ServerManagementController::class, 'suspend'])->name('api.application.servers.suspend');
|
||||
Route::post('/{server:id}/unsuspend', [Application\Servers\ServerManagementController::class, 'unsuspend'])->name('api.application.servers.unsuspend');
|
||||
Route::post('/{server:id}/reinstall', [Application\Servers\ServerManagementController::class, 'reinstall'])->name('api.application.servers.reinstall');
|
||||
Route::post('/{server:id}/transfer', [Application\Servers\ServerManagementController::class, 'startTransfer'])->name('api.application.servers.transfer');
|
||||
Route::post('/{server:id}/transfer/cancel', [Application\Servers\ServerManagementController::class, 'cancelTransfer'])->name('api.application.servers.transfer.cancel');
|
||||
|
||||
Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']);
|
||||
Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']);
|
||||
@@ -97,3 +99,22 @@ Route::prefix('/eggs')->group(function () {
|
||||
Route::get('/', [Application\Eggs\EggController::class, 'index'])->name('api.application.eggs.eggs');
|
||||
Route::get('/{egg:id}', [Application\Eggs\EggController::class, 'view'])->name('api.application.eggs.eggs.view');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Host Controller Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /api/application/database-hosts
|
||||
|
|
||||
*/
|
||||
Route::group(['prefix' => '/database-hosts'], function () {
|
||||
Route::get('/', [Application\DatabaseHosts\DatabaseHostController::class, 'index'])->name('api.application.databasehosts');
|
||||
Route::get('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'view'])->name('api.application.databasehosts.view');
|
||||
|
||||
Route::post('/', [Application\DatabaseHosts\DatabaseHostController::class, 'store']);
|
||||
|
||||
Route::patch('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'update']);
|
||||
|
||||
Route::delete('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'delete']);
|
||||
});
|
||||
|
||||
@@ -1,51 +1,21 @@
|
||||
<?php
|
||||
|
||||
use Dedoc\Scramble\Http\Middleware\RestrictedDocsAccess;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\SecurityScheme;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::group(['prefix' => 'api'], function () {
|
||||
Scramble::extendOpenApi(fn (OpenApi $openApi) => $openApi->secure(SecurityScheme::http('bearer')));
|
||||
Scramble::registerUiRoute(path: 'application', api: 'application');
|
||||
Scramble::registerJsonSpecificationRoute(path: 'application.json', api: 'application');
|
||||
|
||||
Route::view('application', 'scramble::docs', ['api' => 'application'])->name('scramble.docs.api.application');
|
||||
Route::view('client', 'scramble::docs', ['api' => 'client'])->name('scramble.docs.api.client');
|
||||
Route::view('remote', 'scramble::docs', ['api' => 'remote'])->name('scramble.docs.api.remote');
|
||||
Scramble::registerUiRoute(path: 'client', api: 'client');
|
||||
Scramble::registerJsonSpecificationRoute(path: 'client.json', api: 'client');
|
||||
|
||||
Route::get('application.json', function (Dedoc\Scramble\Generator $generator) {
|
||||
config()->set('scramble.api_path', 'api/application');
|
||||
config()->set('scramble.info.description', '
|
||||
These are the Application API endpoints for admins.
|
||||
They let you interact with your Panel on a root basis.
|
||||
');
|
||||
|
||||
return $generator();
|
||||
})->name('scramble.docs.application');
|
||||
|
||||
Route::get('client.json', function (Dedoc\Scramble\Generator $generator) {
|
||||
config()->set('scramble.api_path', 'api/client');
|
||||
config()->set('scramble.info.description', '
|
||||
These are the Client API endpoints for individual users.
|
||||
They let your users interact with your Panel.
|
||||
');
|
||||
|
||||
return $generator();
|
||||
})->name('scramble.docs.client');
|
||||
|
||||
Route::get('remote.json', function (Dedoc\Scramble\Generator $generator) {
|
||||
config()->set('scramble.api_path', 'api/remote');
|
||||
config()->set('scramble.info.description', '
|
||||
These are the Remote API endpoints for Wings.
|
||||
They let Wings interact with your Panel.
|
||||
');
|
||||
|
||||
return $generator();
|
||||
})->name('scramble.docs.remote');
|
||||
Scramble::registerUiRoute(path: 'remote', api: 'remote');
|
||||
Scramble::registerJsonSpecificationRoute(path: 'remote.json', api: 'remote');
|
||||
|
||||
Route::get('', fn () => '
|
||||
<li><a href="/docs/api/application">Application API for Admins</a></li>
|
||||
<li><a href="/docs/api/client">Client API for Users</a></li>
|
||||
<li><a href="/docs/api/remote">Daemon API for Wings</a></li>
|
||||
');
|
||||
})->middleware(config('scramble.middleware', [RestrictedDocsAccess::class]));
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
|
||||
$response = $this->actingAs($user)->postJson("/api/client/servers/$server->uuid/schedules", []);
|
||||
|
||||
$response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
foreach (['name', 'minute', 'hour', 'day_of_month', 'day_of_week'] as $i => $field) {
|
||||
foreach (['name', 'minute', 'hour', 'day_of_month', 'month', 'day_of_week'] as $i => $field) {
|
||||
$response->assertJsonPath("errors.$i.code", 'ValidationException');
|
||||
$response->assertJsonPath("errors.$i.meta.rule", 'required');
|
||||
$response->assertJsonPath("errors.$i.meta.source_field", $field);
|
||||
@@ -67,6 +67,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
|
||||
->postJson("/api/client/servers/$server->uuid/schedules", [
|
||||
'name' => 'Testing',
|
||||
'is_active' => 'no',
|
||||
'only_when_online' => 'false',
|
||||
'minute' => '*',
|
||||
'hour' => '*',
|
||||
'day_of_month' => '*',
|
||||
|
||||
@@ -114,7 +114,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
$this->daemonServerRepository->expects('sync')->withNoArgs()->andReturnUndefined();
|
||||
|
||||
$response = $this->getService()->handle($server, [
|
||||
'oom_disabled' => false,
|
||||
'oom_killer' => false,
|
||||
'memory' => 256,
|
||||
'swap' => 128,
|
||||
'io' => 600,
|
||||
@@ -126,7 +126,7 @@ class BuildModificationServiceTest extends IntegrationTestCase
|
||||
'allocation_limit' => 20,
|
||||
]);
|
||||
|
||||
$this->assertFalse($response->oom_disabled);
|
||||
$this->assertFalse($response->oom_killer);
|
||||
$this->assertSame(256, $response->memory);
|
||||
$this->assertSame(128, $response->swap);
|
||||
$this->assertSame(600, $response->io);
|
||||
|
||||
@@ -138,7 +138,7 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
$this->assertSame($allocations[4]->id, $response->allocations[1]->id);
|
||||
|
||||
$this->assertFalse($response->isSuspended());
|
||||
$this->assertTrue($response->oom_disabled);
|
||||
$this->assertFalse($response->oom_killer);
|
||||
$this->assertSame(0, $response->database_limit);
|
||||
$this->assertSame(0, $response->allocation_limit);
|
||||
$this->assertSame(0, $response->backup_limit);
|
||||
|
||||
Reference in New Issue
Block a user