Compare commits

...

9 Commits

Author SHA1 Message Date
Lance Pioch
65ed06893b Remove unnecessary nullsafe operator on left side of ?? 2026-02-07 23:42:25 -05:00
Lance Pioch
59f21ff833 Use null-safe operator on user() calls for timezone access 2026-02-07 23:23:02 -05:00
Lance Pioch
f2d6b5af76 Allow schedule cron inputs to use a user-selected timezone (#1228)
Add a timezone dropdown (defaulting to the user's profile timezone) to the schedule form so cron inputs are interpreted in the chosen timezone. The selected timezone is used to compute next_run_at in UTC at save time with no database schema changes required.

Resolves #1228
2026-02-06 02:32:21 -05:00
Frogperson
a34bf9fd49 Add Daemon Base Directory field (#2151)
Co-authored-by: Boy132 <mail@boy132.de>
2026-02-05 08:00:25 -05:00
Boy132
7a9deba0e1 Fix notifications for DeleteAction on EditEgg page (#2165) 2026-02-04 22:20:22 +01:00
Charles
159bfe2210 exclude node actions (#2164) 2026-02-04 06:48:13 -05:00
stdpi
a821db8aae Improve file browser UI (#2086) 2026-02-04 05:40:45 -05:00
Lance Pioch
1556f8efb8 Allow all permissions to be toggled at once for api tokens (#2154) 2026-02-02 08:41:10 -05:00
Lance Pioch
57c2aa6f21 Fix the tabbing for username to password on login (#2155)
Co-authored-by: notCharles <charles@pelican.dev>
2026-02-02 08:40:55 -05:00
15 changed files with 121 additions and 39 deletions

View File

@@ -22,6 +22,7 @@ use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -113,12 +114,44 @@ class ApiKeyResource extends Resource
*/
public static function defaultForm(Schema $schema): Schema
{
$permissionList = ApiKey::getPermissionList();
return $schema
->components([
Section::make(trans('admin/apikey.permissions.all'))
->description(trans('admin/apikey.permissions.all_description'))
->columnSpanFull()
->schema([
ToggleButtons::make('permissions_all')
->hiddenLabel()
->inline()
->options([
0 => trans('admin/apikey.permissions.none'),
1 => trans('admin/apikey.permissions.read'),
3 => trans('admin/apikey.permissions.read_write'),
])
->icons([
0 => TablerIcon::BookOff,
1 => TablerIcon::Book,
3 => TablerIcon::Writing,
])
->colors([
0 => 'success',
1 => 'warning',
3 => 'danger',
])
->live()
->afterStateUpdated(function ($state, callable $set) use ($permissionList) {
foreach ($permissionList as $resource) {
$set('permissions_' . $resource, $state);
}
})
->default(0),
]),
Fieldset::make('Permissions')
->columnSpanFull()
->schema(
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
collect($permissionList)->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
->label(str($resource)->replace('_', ' ')->title())->inline()
->options([
0 => trans('admin/apikey.permissions.none'),

View File

@@ -450,17 +450,7 @@ class EditEgg extends EditRecord
return [
DeleteAction::make()
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->tooltip(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use'))
->successNotification(fn (Egg $egg) => Notification::make()
->success()
->title(trans('admin/egg.delete_success'))
->body(trans('admin/egg.deleted', ['egg' => $egg->name]))
)
->failureNotification(fn (Egg $egg) => Notification::make()
->danger()
->title(trans('admin/egg.delete_failed'))
->body(trans('admin/egg.could_not_delete', ['egg' => $egg->name]))
),
->tooltip(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')),
ExportEggAction::make(),
ImportEggAction::make()
->multiple(false),

View File

@@ -278,6 +278,14 @@ class CreateNode extends CreateRecord
->default(256)
->minValue(1)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_base')
->label(trans('admin/node.daemon_base'))
->placeholder('/var/lib/pelican/volumes')
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.daemon_base_help'))
->columnSpan(1)
->required()
->default('/var/lib/pelican/volumes')
->rule('regex:/^([\/][\d\w.\-\/]+)$/'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label(trans('admin/node.sftp_port'))
@@ -287,7 +295,7 @@ class CreateNode extends CreateRecord
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(2)
->columnSpan(1)
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
Grid::make()

View File

@@ -314,7 +314,7 @@ class EditNode extends EditRecord
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 2,
'lg' => 3,
]),
TextInput::make('upload_size')
->columnSpan([
@@ -329,12 +329,24 @@ class EditNode extends EditRecord
->required()
->minValue(1)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_base')
->label(trans('admin/node.daemon_base'))
->placeholder('/var/lib/pelican/volumes')
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.daemon_base_help'))
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 2,
])
->required()
->rule('regex:/^([\/][\d\w.\-\/]+)$/'),
TextInput::make('daemon_sftp')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
'md' => 2,
'lg' => 1,
])
->label(trans('admin/node.sftp_port'))
->minValue(1)
@@ -346,8 +358,8 @@ class EditNode extends EditRecord
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
'md' => 2,
'lg' => 2,
])
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
@@ -356,7 +368,7 @@ class EditNode extends EditRecord
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
'lg' => 2,
])
->label(trans('admin/node.use_for_deploy'))
->inline()
@@ -374,7 +386,7 @@ class EditNode extends EditRecord
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 3,
'lg' => 2,
])
->label(trans('admin/node.maintenance_mode'))
->inline()
@@ -572,7 +584,7 @@ class EditNode extends EditRecord
->columnSpanFull()
->schema([
Actions::make([
Action::make('autoDeploy')
Action::make('exclude_autoDeploy')
->label(trans('admin/node.auto_deploy'))
->color('primary')
->modalHeading(trans('admin/node.auto_deploy'))
@@ -610,7 +622,7 @@ class EditNode extends EditRecord
}),
])->fullWidth(),
Actions::make([
Action::make('resetKey')
Action::make('exclude_resetKey')
->label(trans('admin/node.reset_token'))
->color('danger')
->requiresConfirmation()

View File

@@ -66,6 +66,14 @@ class Login extends BaseLogin
->extraInputAttributes(['tabindex' => 1]);
}
protected function getPasswordFormComponent(): Component
{
/** @var TextInput $component */
$component = parent::getPasswordFormComponent();
return $component->extraInputAttributes(['tabindex' => 2]);
}
protected function getOAuthFormComponent(): Component
{
$actions = [];

View File

@@ -39,13 +39,17 @@ class CreateSchedule extends CreateRecord
$data['server_id'] = $server->id;
}
$timezone = $data['timezone'] ?? user()->timezone ?? 'UTC';
unset($data['timezone']);
if (!isset($data['next_run_at'])) {
$data['next_run_at'] = ScheduleResource::getNextRun(
$data['cron_minute'],
$data['cron_hour'],
$data['cron_day_of_month'],
$data['cron_month'],
$data['cron_day_of_week']
$data['cron_day_of_week'],
$timezone
);
}

View File

@@ -37,12 +37,16 @@ class EditSchedule extends EditRecord
protected function mutateFormDataBeforeSave(array $data): array
{
$timezone = $data['timezone'] ?? user()->timezone ?? 'UTC';
unset($data['timezone']);
$data['next_run_at'] = ScheduleResource::getNextRun(
$data['cron_minute'],
$data['cron_hour'],
$data['cron_day_of_month'],
$data['cron_month'],
$data['cron_day_of_week']
$data['cron_day_of_week'],
$timezone
);
return $data;

View File

@@ -100,13 +100,15 @@ class ScheduleResource extends Resource
Section::make('Cron')
->label(trans('server/schedule.cron'))
->description(function (Get $get) {
$timezone = $get('timezone') ?? user()->timezone ?? 'UTC';
try {
$nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(user()->timezone ?? 'UTC');
$nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'), $timezone)->timezone($timezone);
} catch (Exception) {
$nextRun = trans('server/schedule.invalid');
}
return new HtmlString(trans('server/schedule.cron_body') . '<br>' . trans('server/schedule.cron_timezone', ['timezone' => user()->timezone ?? 'UTC', 'next_run' => $nextRun]));
return new HtmlString(trans('server/schedule.cron_body', ['timezone' => $timezone]) . '<br>' . trans('server/schedule.cron_timezone', ['timezone' => $timezone, 'next_run' => $nextRun]));
})
->schema([
Actions::make([
@@ -295,6 +297,13 @@ class ScheduleResource extends Resource
'default' => 4,
'lg' => 5,
]),
Select::make('timezone')
->label(trans('server/schedule.timezone'))
->options(fn () => array_combine(timezone_identifiers_list(), timezone_identifiers_list()))
->default(user()->timezone ?? 'UTC')
->searchable()
->live()
->hiddenOn('view'),
])
->columnSpanFull(),
]);
@@ -379,10 +388,10 @@ class ScheduleResource extends Resource
];
}
public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek, string $timezone = 'UTC'): Carbon
{
try {
return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek);
return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek, $timezone);
} catch (Exception) {
Notification::make()
->title(trans('server/schedule.notification_invalid_cron'))

View File

@@ -38,11 +38,11 @@ class Utilities
*
* @throws Exception
*/
public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek, string $timezone = 'UTC'): Carbon
{
return Carbon::instance((new CronExpression(
sprintf('%s %s %s %s %s', $minute, $hour, $dayOfMonth, $month, $dayOfWeek)
))->getNextRunDate(now('UTC')));
))->getNextRunDate(now($timezone)))->setTimezone('UTC');
}
public static function checked(string $name, mixed $default): string

View File

@@ -75,7 +75,7 @@ class ScheduleController extends ClientApiController
'cron_minute' => $request->input('minute'),
'is_active' => (bool) $request->input('is_active'),
'only_when_online' => (bool) $request->input('only_when_online'),
'next_run_at' => $this->getNextRunAt($request),
'next_run_at' => $this->getNextRunAt($request, $request->user()->timezone ?? 'UTC'),
]);
Activity::event('server:schedule.create')
@@ -131,7 +131,7 @@ class ScheduleController extends ClientApiController
'cron_minute' => $request->input('minute'),
'is_active' => $active,
'only_when_online' => (bool) $request->input('only_when_online'),
'next_run_at' => $this->getNextRunAt($request),
'next_run_at' => $this->getNextRunAt($request, $request->user()->timezone ?? 'UTC'),
];
// Toggle the processing state of the scheduled task when it is enabled or disabled so that an
@@ -188,7 +188,7 @@ class ScheduleController extends ClientApiController
*
* @throws DisplayException
*/
protected function getNextRunAt(Request $request): Carbon
protected function getNextRunAt(Request $request, string $timezone = 'UTC'): Carbon
{
try {
return Utilities::getScheduleNextRunDate(
@@ -196,7 +196,8 @@ class ScheduleController extends ClientApiController
$request->input('hour'),
$request->input('day_of_month'),
$request->input('month'),
$request->input('day_of_week')
$request->input('day_of_week'),
$timezone
);
} catch (Exception) {
throw new DisplayException('The cron data provided does not evaluate to a valid expression.');

View File

@@ -20,6 +20,8 @@ return [
'never_used' => 'Never Used',
],
'permissions' => [
'all' => 'Set All Permissions',
'all_description' => 'Quickly set all permissions below to the same level.',
'none' => 'None',
'read' => 'Read',
'read_write' => 'Read & Write',

View File

@@ -115,10 +115,6 @@ return [
'no_update_url' => 'The following eggs do not have a working update URL set: :eggs',
'cannot_delete' => 'Cannot delete :count egg(s)',
'eggs_have_servers' => 'The following eggs have servers and cannot be deleted: :eggs',
'delete_success' => 'Egg deleted successfully',
'deleted' => 'Deleted: :egg',
'delete_failed' => 'Failed to delete egg',
'could_not_delete' => 'Could not delete: :egg',
'updated_from' => 'Successfully updated from: :url',
'update_error' => 'Error: :error',
'updated_eggs' => 'Updated: :eggs',

View File

@@ -65,6 +65,8 @@ return [
'sftp_port' => 'SFTP Port',
'sftp_alias' => 'SFTP Alias',
'sftp_alias_help' => 'Display alias for the SFTP address. Leave empty to use the Node FQDN.',
'daemon_base' => 'Daemon Base Directory',
'daemon_base_help' => 'The directory where server data will be stored.',
'use_for_deploy' => 'Use for Deployments?',
'maintenance_mode' => 'Maintenance Mode',
'maintenance_mode_help' => 'If the node is marked \'Under Maintenance\' users won\'t be able to access servers that are on that node',

View File

@@ -29,7 +29,8 @@ return [
'enabled' => 'Enable Schedule?',
'enabled_hint' => 'This schedule will be executed automatically if enabled.',
'cron_body' => 'Please keep in mind that the cron inputs below always assume UTC.',
'timezone' => 'Timezone',
'cron_body' => 'The cron inputs below use your timezone (:timezone).',
'cron_timezone' => 'Next run in your timezone (:timezone): <b> :next_run </b>',
'invalid' => 'Invalid',

View File

@@ -1,4 +1,16 @@
<x-filament-panels::page>
@once
<style>
.files-selection-merged .fi-ta-header-ctn {
position: sticky;
top: 0;
z-index: 1;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
</style>
@endonce
<div
x-data="
{