mirror of
https://github.com/pelican-dev/panel.git
synced 2025-09-09 17:48:45 +02:00
Merge branch 'main' into filament-v4
This commit is contained in:
commit
90a3f38750
@ -275,9 +275,9 @@ class ListServers extends ListRecords
|
||||
} else {
|
||||
return [
|
||||
ActionGroup::make($actions)
|
||||
->icon(fn (Server $server) => $server->condition->getIcon())
|
||||
->color(fn (Server $server) => $server->condition->getColor())
|
||||
->tooltip(fn (Server $server) => $server->condition->getLabel())
|
||||
->icon('tabler-power')
|
||||
->color('primary')
|
||||
->tooltip('Power Actions')
|
||||
->iconSize(IconSize::Large),
|
||||
];
|
||||
}
|
||||
|
53
app/Filament/Components/Actions/CronPresetAction.php
Normal file
53
app/Filament/Components/Actions/CronPresetAction.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Forms\Actions;
|
||||
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
|
||||
class CronPresetAction extends Action
|
||||
{
|
||||
protected string $minute = '0';
|
||||
|
||||
protected string $hour = '0';
|
||||
|
||||
protected string $dayOfMonth = '*';
|
||||
|
||||
protected string $month = '*';
|
||||
|
||||
protected string $dayOfWeek = '*';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->disabled(fn (string $operation) => $operation === 'view');
|
||||
|
||||
$this->color(fn (Get $get) => $get('cron_minute') == $this->minute &&
|
||||
$get('cron_hour') == $this->hour &&
|
||||
$get('cron_day_of_month') == $this->dayOfMonth &&
|
||||
$get('cron_month') == $this->month &&
|
||||
$get('cron_day_of_week') == $this->dayOfWeek
|
||||
? 'success' : 'primary');
|
||||
|
||||
$this->action(function (Set $set) {
|
||||
$set('cron_minute', $this->minute);
|
||||
$set('cron_hour', $this->hour);
|
||||
$set('cron_day_of_month', $this->dayOfMonth);
|
||||
$set('cron_month', $this->month);
|
||||
$set('cron_day_of_week', $this->dayOfWeek);
|
||||
});
|
||||
}
|
||||
|
||||
public function cron(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): static
|
||||
{
|
||||
$this->minute = $minute;
|
||||
$this->hour = $hour;
|
||||
$this->dayOfMonth = $dayOfMonth;
|
||||
$this->month = $month;
|
||||
$this->dayOfWeek = $dayOfWeek;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
34
app/Filament/Components/Actions/ExportScheduleAction.php
Normal file
34
app/Filament/Components/Actions/ExportScheduleAction.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Server;
|
||||
use App\Services\Schedules\Sharing\ScheduleExporterService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
|
||||
class ExportScheduleAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'export';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$this->label(trans('filament-actions::export.modal.actions.export.label'));
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_READ, $server));
|
||||
|
||||
$this->action(fn (ScheduleExporterService $service, Schedule $schedule) => response()->streamDownload(function () use ($service, $schedule) {
|
||||
echo $service->handle($schedule);
|
||||
}, 'schedule-' . str($schedule->name)->kebab()->lower()->trim() . '.json'));
|
||||
}
|
||||
}
|
121
app/Filament/Components/Actions/ImportScheduleAction.php
Normal file
121
app/Filament/Components/Actions/ImportScheduleAction.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Schedules\Sharing\ScheduleImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Arr;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class ImportScheduleAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'import';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$this->label(trans('filament-actions::import.modal.actions.import.label'));
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_CREATE, $server));
|
||||
|
||||
$this->form([
|
||||
Tabs::make('Tabs')
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/schedule.import.file'))
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
->label(trans('admin/schedule.model_label'))
|
||||
->hint(trans('admin/schedule.import.schedule_help'))
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->preserveFilenames()
|
||||
->previewable(false)
|
||||
->storeFiles(false)
|
||||
->multiple(true),
|
||||
]),
|
||||
Tab::make(trans('admin/schedule.import.url'))
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
Repeater::make('urls')
|
||||
->label('')
|
||||
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/schedule-')->before('.json')->headline())
|
||||
->hint(trans('admin/schedule.import.url_help'))
|
||||
->addActionLabel(trans('admin/schedule.import.add_url'))
|
||||
->grid(2)
|
||||
->reorderable(false)
|
||||
->addable(true)
|
||||
->deletable(fn (array $state) => count($state) > 1)
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->live()
|
||||
->label(trans('admin/schedule.import.url'))
|
||||
->url()
|
||||
->endsWith('.json')
|
||||
->validationAttribute(trans('admin/schedule.import.url')),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->action(function (array $data, ScheduleImporterService $service) use ($server) {
|
||||
$schedules = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
|
||||
if (empty($schedules)) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$success, $failed] = [collect(), collect()];
|
||||
|
||||
foreach ($schedules as $schedule) {
|
||||
if ($schedule instanceof TemporaryUploadedFile) {
|
||||
$name = str($schedule->getClientOriginalName())->afterLast('schedule-')->before('.json')->headline();
|
||||
$method = 'fromFile';
|
||||
} else {
|
||||
$schedule = str($schedule);
|
||||
$schedule = $schedule->contains('github.com') ? $schedule->replaceFirst('blob', 'raw') : $schedule;
|
||||
$name = $schedule->afterLast('/schedule-')->before('.json')->headline();
|
||||
$method = 'fromUrl';
|
||||
}
|
||||
try {
|
||||
$service->$method($schedule, $server);
|
||||
$success->push($name);
|
||||
} catch (Exception $exception) {
|
||||
$failed->push($name);
|
||||
report($exception);
|
||||
}
|
||||
}
|
||||
|
||||
if ($failed->count() > 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/schedule.import.import_failed'))
|
||||
->body($failed->join(', '))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
if ($success->count() > 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/schedule.import.import_success'))
|
||||
->body($success->join(', '))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -144,8 +144,12 @@ class Console extends Page
|
||||
#[On('console-status')]
|
||||
public function receivedConsoleUpdate(?string $state = null): void
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($state) {
|
||||
$this->status = ContainerStatus::from($state);
|
||||
cache()->put("servers.$server->uuid.status", $this->status, now()->addSeconds(15));
|
||||
}
|
||||
|
||||
$this->headerActions($this->getHeaderActions());
|
||||
|
@ -7,6 +7,7 @@ use App\Filament\Server\Resources\ScheduleResource\Pages\CreateSchedule;
|
||||
use App\Filament\Server\Resources\ScheduleResource\Pages\ViewSchedule;
|
||||
use App\Filament\Server\Resources\ScheduleResource\Pages\EditSchedule;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Components\Forms\Actions\CronPresetAction;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
|
||||
use App\Helpers\Utilities;
|
||||
@ -19,7 +20,6 @@ use App\Traits\Filament\CanModifyForm;
|
||||
use App\Traits\Filament\CanModifyTable;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
@ -29,19 +29,21 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Schemas\Components\Actions;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Support\Enums\Operation;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Throwable;
|
||||
|
||||
class ScheduleResource extends Resource
|
||||
@ -85,44 +87,20 @@ class ScheduleResource extends Resource
|
||||
{
|
||||
return $schema
|
||||
->columns([
|
||||
'default' => 4,
|
||||
'lg' => 5,
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->components([
|
||||
TextInput::make('name')
|
||||
->columnSpan([
|
||||
'default' => 4,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->label('Schedule Name')
|
||||
->placeholder('A human readable identifier for this schedule.')
|
||||
->autocomplete(false)
|
||||
->required(),
|
||||
// TODO conditional ->hiddenOn, ->visibleOn appear broken?
|
||||
// ToggleButtons::make('Status')
|
||||
// ->hiddenOn(Operation::Create)
|
||||
// ->formatStateUsing(fn (Schedule $schedule) => !$schedule->is_active ? 'inactive' : ($schedule->is_processing ? 'processing' : 'active'))
|
||||
// ->options(fn (Schedule $schedule) => !$schedule->is_active ? ['inactive' => 'Inactive'] : ($schedule->is_processing ? ['processing' => 'Processing'] : ['active' => 'Active']))
|
||||
// ->colors([
|
||||
// 'inactive' => 'danger',
|
||||
// 'processing' => 'warning',
|
||||
// 'active' => 'success',
|
||||
// ])
|
||||
// ->columnSpan([
|
||||
// 'default' => 4,
|
||||
// 'md' => 1,
|
||||
// 'lg' => 1,
|
||||
// ]),
|
||||
Toggle::make('only_when_online')
|
||||
->label('Only when Server is Online?')
|
||||
->hintIconTooltip('Only execute this schedule when the server is in a running state.')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->inline(false)
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->required()
|
||||
->default(1),
|
||||
Toggle::make('is_active')
|
||||
@ -130,97 +108,41 @@ class ScheduleResource extends Resource
|
||||
->hintIconTooltip('This schedule will be executed automatically if enabled.')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->inline(false)
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 2,
|
||||
])
|
||||
->hiddenOn('view')
|
||||
->required()
|
||||
->default(1),
|
||||
TextInput::make('cron_minute')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
ToggleButtons::make('Status')
|
||||
->formatStateUsing(fn (Schedule $schedule) => !$schedule->is_active ? 'inactive' : ($schedule->is_processing ? 'processing' : 'active'))
|
||||
->options(fn (Schedule $schedule) => !$schedule->is_active ? ['inactive' => 'Inactive'] : ($schedule->is_processing ? ['processing' => 'Processing'] : ['active' => 'Active']))
|
||||
->colors([
|
||||
'inactive' => 'danger',
|
||||
'processing' => 'warning',
|
||||
'active' => 'success',
|
||||
])
|
||||
->label('Minute')
|
||||
->default('*/5')
|
||||
->required(),
|
||||
TextInput::make('cron_hour')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Hour')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_day_of_month')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Day of Month')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_month')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Month')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_day_of_week')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Day of Week')
|
||||
->default('*')
|
||||
->required(),
|
||||
Section::make('Presets')
|
||||
->hiddenOn('view')
|
||||
->columns(1)
|
||||
->columnSpanFull()
|
||||
->visibleOn('view'),
|
||||
Section::make('Cron')
|
||||
->description(fn (Get $get) => new HtmlString('Please keep in mind that the cron inputs below always assume UTC.<br>Next run in your timezone (' . auth()->user()->timezone . '): <b>'. Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(auth()->user()->timezone) . '</b>'))
|
||||
->schema([
|
||||
Actions::make([
|
||||
Action::make('hourly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '*');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('daily')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('weekly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '0');
|
||||
}),
|
||||
Action::make('monthly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '1');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '0');
|
||||
}),
|
||||
Action::make('every_x_minutes')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->schema([
|
||||
CronPresetAction::make('hourly')
|
||||
->cron('0', '*', '*', '*', '*'),
|
||||
CronPresetAction::make('daily')
|
||||
->cron('0', '0', '*', '*', '*'),
|
||||
CronPresetAction::make('weekly_monday')
|
||||
->label('Weekly (Monday)')
|
||||
->cron('0', '0', '*', '*', '1'),
|
||||
CronPresetAction::make('weekly_sunday')
|
||||
->label('Weekly (Sunday)')
|
||||
->cron('0', '0', '*', '*', '0'),
|
||||
CronPresetAction::make('monthly')
|
||||
->cron('0', '0', '1', '*', '*'),
|
||||
CronPresetAction::make('every_x_minutes')
|
||||
->color(fn (Get $get) => str($get('cron_minute'))->startsWith('*/')
|
||||
&& $get('cron_hour') == '*'
|
||||
&& $get('cron_day_of_month') == '*'
|
||||
&& $get('cron_month') == '*'
|
||||
&& $get('cron_day_of_week') == '*' ? 'success' : 'primary')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
@ -236,9 +158,13 @@ class ScheduleResource extends Resource
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_hours')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->schema([
|
||||
CronPresetAction::make('every_x_hours')
|
||||
->color(fn (Get $get) => $get('cron_minute') == '0'
|
||||
&& str($get('cron_hour'))->startsWith('*/')
|
||||
&& $get('cron_day_of_month') == '*'
|
||||
&& $get('cron_month') == '*'
|
||||
&& $get('cron_day_of_week') == '*' ? 'success' : 'primary')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
@ -254,9 +180,13 @@ class ScheduleResource extends Resource
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_days')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->schema([
|
||||
CronPresetAction::make('every_x_days')
|
||||
->color(fn (Get $get) => $get('cron_minute') == '0'
|
||||
&& $get('cron_hour') == '0'
|
||||
&& str($get('cron_day_of_month'))->startsWith('*/')
|
||||
&& $get('cron_month') == '*'
|
||||
&& $get('cron_day_of_week') == '*' ? 'success' : 'primary')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
@ -272,9 +202,13 @@ class ScheduleResource extends Resource
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_months')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->schema([
|
||||
CronPresetAction::make('every_x_months')
|
||||
->color(fn (Get $get) => $get('cron_minute') == '0'
|
||||
&& $get('cron_hour') == '0'
|
||||
&& $get('cron_day_of_month') == '1'
|
||||
&& str($get('cron_month'))->startsWith('*/')
|
||||
&& $get('cron_day_of_week') == '*' ? 'success' : 'primary')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
@ -290,9 +224,13 @@ class ScheduleResource extends Resource
|
||||
$set('cron_month', '*/' . $data['x']);
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_day_of_week')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->schema([
|
||||
CronPresetAction::make('every_x_day_of_week')
|
||||
->color(fn (Get $get) => $get('cron_minute') == '0'
|
||||
&& $get('cron_hour') == '0'
|
||||
&& $get('cron_day_of_month') == '*'
|
||||
&& $get('cron_month') == '*'
|
||||
&& $get('cron_day_of_week') != '*' ? 'success' : 'primary')
|
||||
->form([
|
||||
Select::make('x')
|
||||
->label('')
|
||||
->prefix('Every')
|
||||
@ -315,6 +253,42 @@ class ScheduleResource extends Resource
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', $data['x']);
|
||||
}),
|
||||
])
|
||||
->hiddenOn('view'),
|
||||
Group::make([
|
||||
TextInput::make('cron_minute')
|
||||
->label('Minute')
|
||||
->default('*/5')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('cron_hour')
|
||||
->label('Hour')
|
||||
->default('*')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('cron_day_of_month')
|
||||
->label('Day of Month')
|
||||
->default('*')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('cron_month')
|
||||
->label('Month')
|
||||
->default('*')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('cron_day_of_week')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Day of Week')
|
||||
->default('*')
|
||||
->required()
|
||||
->live(),
|
||||
])
|
||||
->columns([
|
||||
'default' => 4,
|
||||
'lg' => 5,
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Components\Actions\ExportScheduleAction;
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Models\Schedule;
|
||||
use Filament\Actions\Action;
|
||||
@ -52,6 +53,7 @@ class EditSchedule extends EditRecord
|
||||
->property('name', $record->name)
|
||||
->log();
|
||||
}),
|
||||
ExportScheduleAction::make(),
|
||||
$this->getSaveFormAction()->formId('form')->label('Save'),
|
||||
$this->getCancelFormAction()->formId('form'),
|
||||
];
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Filament\Components\Actions\ImportScheduleAction;
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
@ -23,6 +24,7 @@ class ListSchedules extends ListRecords
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('New Schedule'),
|
||||
ImportScheduleAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ class DatabaseHostController extends ApplicationApiController
|
||||
*/
|
||||
public function index(GetDatabaseHostRequest $request): array
|
||||
{
|
||||
$databases = QueryBuilder::for(DatabaseHost::query())
|
||||
$databases = QueryBuilder::for(DatabaseHost::class)
|
||||
->allowedFilters(['name', 'host'])
|
||||
->allowedSorts(['id', 'name', 'host'])
|
||||
->paginate($request->query('per_page') ?? 10);
|
||||
|
@ -16,6 +16,12 @@ use App\Http\Requests\Api\Application\Mounts\StoreMountRequest;
|
||||
use App\Http\Requests\Api\Application\Mounts\DeleteMountRequest;
|
||||
use App\Http\Requests\Api\Application\Mounts\UpdateMountRequest;
|
||||
use App\Exceptions\Service\HasActiveServersException;
|
||||
use App\Http\Requests\Api\Application\Eggs\GetEggsRequest;
|
||||
use App\Http\Requests\Api\Application\Nodes\GetNodesRequest;
|
||||
use App\Http\Requests\Api\Application\Servers\GetServerRequest;
|
||||
use App\Transformers\Api\Application\EggTransformer;
|
||||
use App\Transformers\Api\Application\NodeTransformer;
|
||||
use App\Transformers\Api\Application\ServerTransformer;
|
||||
|
||||
class MountController extends ApplicationApiController
|
||||
{
|
||||
@ -28,7 +34,7 @@ class MountController extends ApplicationApiController
|
||||
*/
|
||||
public function index(GetMountRequest $request): array
|
||||
{
|
||||
$mounts = QueryBuilder::for(Mount::query())
|
||||
$mounts = QueryBuilder::for(Mount::class)
|
||||
->allowedFilters(['uuid', 'name'])
|
||||
->allowedSorts(['id', 'uuid'])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
@ -115,6 +121,42 @@ class MountController extends ApplicationApiController
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* List assigned eggs
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function getEggs(GetEggsRequest $request, Mount $mount): array
|
||||
{
|
||||
return $this->fractal->collection($mount->eggs)
|
||||
->transformWith($this->getTransformer(EggTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* List assigned nodes
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function getNodes(GetNodesRequest $request, Mount $mount): array
|
||||
{
|
||||
return $this->fractal->collection($mount->nodes)
|
||||
->transformWith($this->getTransformer(NodeTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* List assigned servers
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function getServers(GetServerRequest $request, Mount $mount): array
|
||||
{
|
||||
return $this->fractal->collection($mount->servers)
|
||||
->transformWith($this->getTransformer(ServerTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign eggs to mount
|
||||
*
|
||||
@ -125,13 +167,11 @@ class MountController extends ApplicationApiController
|
||||
public function addEggs(Request $request, Mount $mount): array
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'eggs' => 'required|exists:eggs,id',
|
||||
'eggs' => 'required|array|exists:eggs,id',
|
||||
'eggs.*' => 'integer',
|
||||
]);
|
||||
|
||||
$eggs = $validatedData['eggs'] ?? [];
|
||||
if (count($eggs) > 0) {
|
||||
$mount->eggs()->attach($eggs);
|
||||
}
|
||||
$mount->eggs()->attach($validatedData['eggs']);
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
@ -139,7 +179,7 @@ class MountController extends ApplicationApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign mounts to mount
|
||||
* Assign nodes to mount
|
||||
*
|
||||
* Adds nodes to the mount's many-to-many relation.
|
||||
*
|
||||
@ -147,13 +187,34 @@ class MountController extends ApplicationApiController
|
||||
*/
|
||||
public function addNodes(Request $request, Mount $mount): array
|
||||
{
|
||||
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
|
||||
$validatedData = $request->validate([
|
||||
'nodes' => 'required|array|exists:nodes,id',
|
||||
'nodes.*' => 'integer',
|
||||
]);
|
||||
|
||||
$nodes = $data['nodes'] ?? [];
|
||||
if (count($nodes) > 0) {
|
||||
$mount->nodes()->attach($nodes);
|
||||
$mount->nodes()->attach($validatedData['nodes']);
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign servers to mount
|
||||
*
|
||||
* Adds servers to the mount's many-to-many relation.
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function addServers(Request $request, Mount $mount): array
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'servers' => 'required|array|exists:servers,id',
|
||||
'servers.*' => 'integer',
|
||||
]);
|
||||
|
||||
$mount->servers()->attach($validatedData['servers']);
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
@ -182,4 +243,16 @@ class MountController extends ApplicationApiController
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign server from mount
|
||||
*
|
||||
* Deletes a server from the mount's many-to-many relation.
|
||||
*/
|
||||
public function deleteServer(Mount $mount, int $server_id): JsonResponse
|
||||
{
|
||||
$mount->servers()->detach($server_id);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class NodeController extends ApplicationApiController
|
||||
*/
|
||||
public function index(GetNodesRequest $request): array
|
||||
{
|
||||
$nodes = QueryBuilder::for(Node::query())
|
||||
$nodes = QueryBuilder::for(Node::class)
|
||||
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
|
||||
->allowedSorts(['id', 'uuid', 'memory', 'disk', 'cpu'])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
@ -27,7 +27,7 @@ class RoleController extends ApplicationApiController
|
||||
*/
|
||||
public function index(GetRoleRequest $request): array
|
||||
{
|
||||
$roles = QueryBuilder::for(Role::query())
|
||||
$roles = QueryBuilder::for(Role::class)
|
||||
->allowedFilters(['id', 'name'])
|
||||
->allowedSorts(['id', 'name'])
|
||||
->paginate($request->query('per_page') ?? 10);
|
||||
|
@ -43,7 +43,7 @@ class ServerController extends ApplicationApiController
|
||||
*/
|
||||
public function index(GetServersRequest $request): array
|
||||
{
|
||||
$servers = QueryBuilder::for(Server::query())
|
||||
$servers = QueryBuilder::for(Server::class)
|
||||
->allowedFilters(['uuid', 'uuid_short', 'name', 'description', 'image', 'external_id'])
|
||||
->allowedSorts(['id', 'uuid'])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
@ -43,7 +43,7 @@ class UserController extends ApplicationApiController
|
||||
*/
|
||||
public function index(GetUsersRequest $request): array
|
||||
{
|
||||
$users = QueryBuilder::for(User::query())
|
||||
$users = QueryBuilder::for(User::class)
|
||||
->allowedFilters(['email', 'uuid', 'username', 'external_id'])
|
||||
->allowedSorts(['id', 'uuid'])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
@ -11,6 +11,8 @@ use App\Models\Filters\MultiFieldServerFilter;
|
||||
use App\Transformers\Api\Client\ServerTransformer;
|
||||
use App\Http\Requests\Api\Client\GetServersRequest;
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[Group('Base')]
|
||||
class ClientController extends ClientApiController
|
||||
@ -36,10 +38,11 @@ class ClientController extends ClientApiController
|
||||
$user = $request->user();
|
||||
$transformer = $this->getTransformer(ServerTransformer::class);
|
||||
|
||||
/** @var Builder<Model> $query */
|
||||
$query = Server::query()->with($this->getIncludesForTransformer($transformer, ['node']));
|
||||
|
||||
// Start the query builder and ensure we eager load any requested relationships from the request.
|
||||
$builder = QueryBuilder::for(
|
||||
Server::query()->with($this->getIncludesForTransformer($transformer, ['node']))
|
||||
)->allowedFilters([
|
||||
$builder = QueryBuilder::for($query)->allowedFilters([
|
||||
'uuid',
|
||||
'name',
|
||||
'description',
|
||||
|
@ -143,15 +143,7 @@ class SubuserController extends ClientApiController
|
||||
*/
|
||||
protected function getDefaultPermissions(Request $request): array
|
||||
{
|
||||
$allowed = Permission::permissions()
|
||||
->map(function ($value, $prefix) {
|
||||
return array_map(function ($value) use ($prefix) {
|
||||
return "$prefix.$value";
|
||||
}, array_keys($value['keys']));
|
||||
})
|
||||
->flatten()
|
||||
->all();
|
||||
|
||||
$allowed = Permission::permissionKeys()->all();
|
||||
$cleaned = array_intersect($request->input('permissions') ?? [], $allowed);
|
||||
|
||||
return array_unique(array_merge($cleaned, [Permission::ACTION_WEBSOCKET_CONNECT]));
|
||||
|
@ -55,6 +55,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
|
||||
// Automatic deployment rules
|
||||
'deploy' => 'sometimes|required|array',
|
||||
// Locations are deprecated, use tags
|
||||
'deploy.locations' => 'sometimes|array',
|
||||
'deploy.locations.*' => 'required_with:deploy.locations|integer|min:1',
|
||||
'deploy.tags' => 'array',
|
||||
@ -176,7 +177,6 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
$object->setDedicated($this->input('deploy.dedicated_ip', false));
|
||||
$object->setTags($this->input('deploy.tags', $this->input('deploy.locations', [])));
|
||||
$object->setPorts($this->input('deploy.port_range', []));
|
||||
$object->setNode($this->input('deploy.node_id'));
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ class SendPowerRequest extends ClientApiRequest
|
||||
return Permission::ACTION_CONTROL_RESTART;
|
||||
}
|
||||
|
||||
return '__invalid';
|
||||
// Fallback for invalid signals
|
||||
return Permission::ACTION_WEBSOCKET_CONNECT;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,6 +8,10 @@ use Illuminate\Auth\Events\Login;
|
||||
|
||||
class AuthenticationListener
|
||||
{
|
||||
private const PROTECTED_FIELDS = [
|
||||
'password', 'token', 'secret',
|
||||
];
|
||||
|
||||
/**
|
||||
* Handles an authentication event by logging the user and information about
|
||||
* the request.
|
||||
@ -22,9 +26,11 @@ class AuthenticationListener
|
||||
|
||||
if ($event instanceof Failed) {
|
||||
foreach ($event->credentials as $key => $value) {
|
||||
if (!in_array($key, self::PROTECTED_FIELDS, true)) {
|
||||
$activity = $activity->property($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$activity->event($event instanceof Failed ? 'auth:fail' : 'auth:success')->log();
|
||||
}
|
||||
|
@ -2,12 +2,8 @@
|
||||
|
||||
namespace App\Models\Objects;
|
||||
|
||||
use App\Models\Node;
|
||||
|
||||
class DeploymentObject
|
||||
{
|
||||
private ?Node $node = null;
|
||||
|
||||
private bool $dedicated = false;
|
||||
|
||||
/** @var string[] */
|
||||
@ -16,18 +12,6 @@ class DeploymentObject
|
||||
/** @var array<int|string> */
|
||||
private array $ports = [];
|
||||
|
||||
public function getNode(): ?Node
|
||||
{
|
||||
return $this->node;
|
||||
}
|
||||
|
||||
public function setNode(Node $node): self
|
||||
{
|
||||
$this->node = $node;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDedicated(): bool
|
||||
{
|
||||
return $this->dedicated;
|
||||
|
@ -211,4 +211,11 @@ class Permission extends Model implements Validatable
|
||||
|
||||
return collect($permissions);
|
||||
}
|
||||
|
||||
public static function permissionKeys(): Collection
|
||||
{
|
||||
return static::permissions()
|
||||
->map(fn ($value, $prefix) => array_map(fn ($value) => "$prefix.$value", array_keys($value['keys'])))
|
||||
->flatten();
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
|
||||
@ -21,6 +22,7 @@ class ServerPolicy
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Permission::permissionKeys()->contains($ability)) {
|
||||
// Owner has full server permissions
|
||||
if ($server->owner_id === $user->id) {
|
||||
return true;
|
||||
@ -31,6 +33,7 @@ class ServerPolicy
|
||||
if ($subuser && in_array($ability, $subuser->permissions)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure user can target node of the server
|
||||
if (!$user->canTarget($server->node)) {
|
||||
|
39
app/Services/Schedules/Sharing/ScheduleExporterService.php
Normal file
39
app/Services/Schedules/Sharing/ScheduleExporterService.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Schedules\Sharing;
|
||||
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Task;
|
||||
|
||||
class ScheduleExporterService
|
||||
{
|
||||
public function handle(Schedule|int $schedule): string
|
||||
{
|
||||
if (!$schedule instanceof Schedule) {
|
||||
$schedule = Schedule::findOrFail($schedule);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $schedule->name,
|
||||
'is_active' => $schedule->is_active,
|
||||
'only_when_online' => $schedule->only_when_online,
|
||||
'cron_minute' => $schedule->cron_minute,
|
||||
'cron_hour' => $schedule->cron_hour,
|
||||
'cron_day_of_month' => $schedule->cron_day_of_month,
|
||||
'cron_month' => $schedule->cron_month,
|
||||
'cron_day_of_week' => $schedule->cron_day_of_week,
|
||||
|
||||
'tasks' => $schedule->tasks->map(function (Task $task) {
|
||||
return [
|
||||
'sequence_id' => $task->sequence_id,
|
||||
'action' => $task->action,
|
||||
'payload' => $task->payload,
|
||||
'time_offset' => $task->time_offset,
|
||||
'continue_on_failure' => $task->continue_on_failure,
|
||||
];
|
||||
}),
|
||||
];
|
||||
|
||||
return json_encode($data, JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
78
app/Services/Schedules/Sharing/ScheduleImporterService.php
Normal file
78
app/Services/Schedules/Sharing/ScheduleImporterService.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Schedules\Sharing;
|
||||
|
||||
use App\Exceptions\Service\InvalidFileUploadException;
|
||||
use App\Helpers\Utilities;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Server;
|
||||
use App\Models\Task;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
|
||||
class ScheduleImporterService
|
||||
{
|
||||
public function __construct(protected ConnectionInterface $connection) {}
|
||||
|
||||
public function fromFile(UploadedFile $file, Server $server): Schedule
|
||||
{
|
||||
if ($file->getError() !== UPLOAD_ERR_OK) {
|
||||
throw new InvalidFileUploadException('The selected file was not uploaded successfully');
|
||||
}
|
||||
|
||||
try {
|
||||
$parsed = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $exception) {
|
||||
throw new InvalidFileUploadException('Could not read JSON file: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return $this->connection->transaction(function () use ($server, $parsed) {
|
||||
$minute = Arr::get($parsed, 'cron_minute', '0');
|
||||
$hour = Arr::get($parsed, 'cron_hour', '0');
|
||||
$dayOfMonth = Arr::get($parsed, 'cron_day_of_month', '*');
|
||||
$month = Arr::get($parsed, 'cron_month', '*');
|
||||
$dayOfWeek = Arr::get($parsed, 'cron_day_of_week', '*');
|
||||
|
||||
$schedule = Schedule::create([
|
||||
'server_id' => $server->id,
|
||||
'name' => Arr::get($parsed, 'name'),
|
||||
'is_active' => Arr::get($parsed, 'is_active'),
|
||||
'only_when_online' => Arr::get($parsed, 'only_when_online'),
|
||||
'cron_minute' => $minute,
|
||||
'cron_hour' => $hour,
|
||||
'cron_day_of_month' => $dayOfMonth,
|
||||
'cron_month' => $month,
|
||||
'cron_day_of_week' => $dayOfWeek,
|
||||
'next_run_at' => Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek),
|
||||
]);
|
||||
|
||||
foreach (Arr::get($parsed, 'tasks', []) as $task) {
|
||||
Task::create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'sequence_id' => Arr::get($task, 'sequence_id'),
|
||||
'action' => Arr::get($task, 'action'),
|
||||
'payload' => Arr::get($task, 'payload'),
|
||||
'time_offset' => Arr::get($task, 'time_offset'),
|
||||
'continue_on_failure' => Arr::get($task, 'continue_on_failure'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $schedule;
|
||||
});
|
||||
}
|
||||
|
||||
public function fromUrl(string $url, Server $server): Schedule
|
||||
{
|
||||
$info = pathinfo($url);
|
||||
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
|
||||
$tmpPath = $tmpDir->path($info['basename']);
|
||||
|
||||
if (!file_put_contents($tmpPath, file_get_contents($url))) {
|
||||
throw new InvalidFileUploadException('Could not write temporary file.');
|
||||
}
|
||||
|
||||
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'), $server);
|
||||
}
|
||||
}
|
@ -72,19 +72,35 @@ class ServerCreationService
|
||||
$data['image'] = $data['image'] ?? collect($egg->docker_images)->first();
|
||||
$data['startup'] = $data['startup'] ?? $egg->startup;
|
||||
|
||||
// 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 a deployment object has been passed we need to get the allocation and node that the server should use.
|
||||
if ($deployment) {
|
||||
$allocation = $this->configureDeployment($data, $deployment);
|
||||
$nodes = $this->findViableNodesService->handle(
|
||||
Arr::get($data, 'memory', 0),
|
||||
Arr::get($data, 'disk', 0),
|
||||
Arr::get($data, 'cpu', 0),
|
||||
$deployment->getTags(),
|
||||
)->pluck('id');
|
||||
|
||||
if ($nodes->isEmpty()) {
|
||||
throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes'));
|
||||
}
|
||||
|
||||
$ports = $deployment->getPorts();
|
||||
if (!empty($ports)) {
|
||||
$allocation = $this->allocationSelectionService->setDedicated($deployment->isDedicated())
|
||||
->setNodes($nodes->toArray())
|
||||
->setPorts($ports)
|
||||
->handle();
|
||||
|
||||
if ($allocation) {
|
||||
$data['allocation_id'] = $allocation->id;
|
||||
// Auto-configure the node based on the selected allocation
|
||||
// if no node was defined.
|
||||
$data['node_id'] = $allocation->node_id;
|
||||
}
|
||||
$data['node_id'] ??= $deployment->getNode()->id;
|
||||
|
||||
if (empty($data['node_id'])) {
|
||||
$data['node_id'] = $nodes->first();
|
||||
}
|
||||
}
|
||||
|
||||
Assert::false(empty($data['node_id']), 'Expected a non-empty node_id in server creation data.');
|
||||
|
||||
$eggVariableData = $this->validatorService
|
||||
@ -123,39 +139,6 @@ class ServerCreationService
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an allocation to use for automatic deployment.
|
||||
*
|
||||
* @param array{memory?: ?int, disk?: ?int, cpu?: ?int, tags?: ?string[]} $data
|
||||
*
|
||||
* @throws DisplayException
|
||||
* @throws NoViableAllocationException
|
||||
*/
|
||||
private function configureDeployment(array $data, DeploymentObject $deployment): ?Allocation
|
||||
{
|
||||
$nodes = $this->findViableNodesService->handle(
|
||||
Arr::get($data, 'memory', 0),
|
||||
Arr::get($data, 'disk', 0),
|
||||
Arr::get($data, 'cpu', 0),
|
||||
$deployment->getTags(),
|
||||
);
|
||||
|
||||
$availableNodes = $nodes->pluck('id');
|
||||
|
||||
if ($availableNodes->isEmpty()) {
|
||||
throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes'));
|
||||
}
|
||||
|
||||
if (!$deployment->getPorts()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->allocationSelectionService->setDedicated($deployment->isDedicated())
|
||||
->setNodes($availableNodes->toArray())
|
||||
->setPorts($deployment->getPorts())
|
||||
->handle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the server in the database and return the model.
|
||||
*
|
||||
|
@ -36,7 +36,7 @@ class VariableValidatorService
|
||||
|
||||
$data = $rules = $customAttributes = [];
|
||||
foreach ($variables as $variable) {
|
||||
$data['environment'][$variable->env_variable] = array_get($fields, $variable->env_variable);
|
||||
$data['environment'][$variable->env_variable] = $fields[$variable->env_variable] ?? $variable->default_value;
|
||||
$rules['environment.' . $variable->env_variable] = $variable->rules;
|
||||
$customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]);
|
||||
}
|
||||
|
@ -9,15 +9,16 @@ use Ramsey\Uuid\Uuid;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Hashing\Hasher;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Contracts\Auth\PasswordBroker;
|
||||
use App\Notifications\AccountCreated;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Auth\Passwords\PasswordBroker;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
|
||||
class UserCreationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConnectionInterface $connection,
|
||||
private readonly Hasher $hasher,
|
||||
private readonly PasswordBroker $passwordBroker,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -53,7 +54,9 @@ class UserCreationService
|
||||
}
|
||||
|
||||
if (isset($generateResetToken)) {
|
||||
$token = $this->passwordBroker->createToken($user);
|
||||
/** @var PasswordBroker $broker */
|
||||
$broker = Password::broker(Filament::getPanel('app')->getAuthPasswordBroker());
|
||||
$token = $broker->createToken($user);
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
|
@ -16,7 +16,7 @@
|
||||
"doctrine/dbal": "~3.6.0",
|
||||
"filament/filament": "~4.0",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"laravel/framework": "^12.19",
|
||||
"laravel/framework": "^12.20",
|
||||
"laravel/helpers": "^1.7",
|
||||
"laravel/sanctum": "^4.1",
|
||||
"laravel/socialite": "^5.21",
|
||||
|
10
composer.lock
generated
10
composer.lock
generated
@ -14241,16 +14241,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.1.17",
|
||||
"version": "2.1.18",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
|
||||
"reference": "ee1f390b7a70cdf74a2b737e554f68afea885db7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
|
||||
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/ee1f390b7a70cdf74a2b737e554f68afea885db7",
|
||||
"reference": "ee1f390b7a70cdf74a2b737e554f68afea885db7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -14295,7 +14295,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-05-21T20:55:28+00:00"
|
||||
"time": "2025-07-17T17:22:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::transaction(function () {
|
||||
$logs = ActivityLog::where('event', 'auth:fail')->get();
|
||||
foreach ($logs as $log) {
|
||||
$log->update(['properties' => collect($log->properties)->except(['password'])->toArray()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Not needed
|
||||
}
|
||||
};
|
15
lang/en/admin/schedule.php
Normal file
15
lang/en/admin/schedule.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'model_label' => 'Schedule',
|
||||
'model_label_plural' => 'Schedule',
|
||||
'import' => [
|
||||
'file' => 'File',
|
||||
'url' => 'URL',
|
||||
'schedule_help' => 'This should be the raw .json file ( schedule-daily-restart.json )',
|
||||
'url_help' => 'URLs must point directly to the raw .json file',
|
||||
'add_url' => 'New URL',
|
||||
'import_failed' => 'Import Failed',
|
||||
'import_success' => 'Import Success',
|
||||
],
|
||||
];
|
@ -90,7 +90,11 @@
|
||||
|
||||
terminal.open(document.getElementById('terminal'));
|
||||
|
||||
fitAddon.fit(); //Fit on first load
|
||||
fitAddon.fit(); // Fixes SPA issues.
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
fitAddon.fit();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
fitAddon.fit();
|
||||
|
@ -13,7 +13,7 @@
|
||||
:icon="$server->condition->getIcon()"
|
||||
:color="$server->condition->getColor()"
|
||||
:tooltip="$server->condition->getLabel()"
|
||||
size="xl"
|
||||
size="lg"
|
||||
/>
|
||||
<h2 class="text-xl font-bold">
|
||||
{{ $server->name }}
|
||||
@ -21,6 +21,15 @@
|
||||
({{ $server->formatResource('uptime', type: \App\Enums\ServerResourceType::Time) }})
|
||||
</span>
|
||||
</h2>
|
||||
<div class="end-0" x-on:click.stop>
|
||||
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-b-lg overflow-hidden p-1">
|
||||
<x-filament-tables::actions
|
||||
:actions="\App\Filament\App\Resources\ServerResource\Pages\ListServers::getPowerActions(view: 'grid')"
|
||||
:alignment="\Filament\Support\Enums\Alignment::Center"
|
||||
:record="$server"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-center items-center gap-4">
|
||||
|
@ -133,16 +133,21 @@ Route::prefix('/database-hosts')->group(function () {
|
||||
Route::prefix('mounts')->group(function () {
|
||||
Route::get('/', [Application\Mounts\MountController::class, 'index'])->name('api.application.mounts');
|
||||
Route::get('/{mount:id}', [Application\Mounts\MountController::class, 'view'])->name('api.application.mounts.view');
|
||||
Route::get('/{mount:id}/eggs', [Application\Mounts\MountController::class, 'getEggs']);
|
||||
Route::get('/{mount:id}/nodes', [Application\Mounts\MountController::class, 'getNodes']);
|
||||
Route::get('/{mount:id}/servers', [Application\Mounts\MountController::class, 'getServers']);
|
||||
|
||||
Route::post('/', [Application\Mounts\MountController::class, 'store']);
|
||||
Route::post('/{mount:id}/eggs', [Application\Mounts\MountController::class, 'addEggs'])->name('api.application.mounts.eggs');
|
||||
Route::post('/{mount:id}/nodes', [Application\Mounts\MountController::class, 'addNodes'])->name('api.application.mounts.nodes');
|
||||
Route::post('/{mount:id}/servers', [Application\Mounts\MountController::class, 'addServers'])->name('api.application.mounts.servers');
|
||||
|
||||
Route::patch('/{mount:id}', [Application\Mounts\MountController::class, 'update']);
|
||||
|
||||
Route::delete('/{mount:id}', [Application\Mounts\MountController::class, 'delete']);
|
||||
Route::delete('/{mount:id}/eggs/{egg_id}', [Application\Mounts\MountController::class, 'deleteEgg']);
|
||||
Route::delete('/{mount:id}/nodes/{node_id}', [Application\Mounts\MountController::class, 'deleteNode']);
|
||||
Route::delete('/{mount:id}/servers/{server_id}', [Application\Mounts\MountController::class, 'deleteServer']);
|
||||
});
|
||||
|
||||
/*
|
||||
|
@ -116,6 +116,7 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
$this->assertInstanceOf(Server::class, $response);
|
||||
$this->assertNotNull($response->uuid);
|
||||
$this->assertSame($response->uuid_short, substr($response->uuid, 0, 8));
|
||||
$this->assertSame($node->id, $response->node_id);
|
||||
$this->assertSame($egg->id, $response->egg_id);
|
||||
$this->assertCount(2, $response->variables);
|
||||
$this->assertSame('123', $response->variables()->firstWhere('env_variable', 'BUNGEE_VERSION')->server_value);
|
||||
@ -153,7 +154,7 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
/** @var \App\Models\Node $node */
|
||||
$node = Node::factory()->create();
|
||||
|
||||
$deployment = (new DeploymentObject())->setNode($node);
|
||||
$deployment = new DeploymentObject();
|
||||
|
||||
$egg = $this->cloneEggAndVariables($this->bungeecord);
|
||||
// We want to make sure that the validator service runs as an admin, and not as a regular
|
||||
@ -204,6 +205,7 @@ class ServerCreationServiceTest extends IntegrationTestCase
|
||||
$this->assertInstanceOf(Server::class, $response);
|
||||
$this->assertNotNull($response->uuid);
|
||||
$this->assertSame($response->uuid_short, substr($response->uuid, 0, 8));
|
||||
$this->assertSame($node->id, $response->node_id);
|
||||
$this->assertSame($egg->id, $response->egg_id);
|
||||
$this->assertCount(2, $response->variables);
|
||||
$this->assertSame('123', $response->variables()->firstWhere('env_variable', 'BUNGEE_VERSION')->server_value);
|
||||
|
@ -34,6 +34,7 @@ class VariableValidatorServiceTest extends IntegrationTestCase
|
||||
try {
|
||||
$this->getService()->handle($egg->id, [
|
||||
'BUNGEE_VERSION' => '1.2.3',
|
||||
'SERVER_JARFILE' => '',
|
||||
]);
|
||||
|
||||
$this->fail('This statement should not be reached.');
|
||||
|
Loading…
x
Reference in New Issue
Block a user