disaster of a merge.... main->filament-v4

This commit is contained in:
Charles 2025-06-23 07:10:27 -04:00
commit 5121cf7170
186 changed files with 3438 additions and 2589 deletions

View File

@ -35,7 +35,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: [8.2, 8.3, 8.4] php: [ 8.2, 8.3, 8.4 ]
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -68,4 +68,4 @@ jobs:
run: composer install --no-interaction --no-suggest --no-progress --no-scripts run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: PHPStan - name: PHPStan
run: vendor/bin/phpstan --memory-limit=-1 run: vendor/bin/phpstan --memory-limit=-1 --error-format=github

1
.gitignore vendored
View File

@ -1,7 +1,6 @@
/.phpunit.cache /.phpunit.cache
/node_modules /node_modules
/public/build /public/build
/public/hot
/public/storage /public/storage
/storage/*.key /storage/*.key
/storage/pail /storage/pail

View File

@ -75,14 +75,15 @@ RUN chown root:www-data ./ \
&& chmod 750 ./ \ && chmod 750 ./ \
# Files should not have execute set, but directories need it # Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \ && find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories # Create necessary directories
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \ && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, and avatars # Symlinks for env, database, and avatars
&& ln -s /pelican-data/.env ./.env \ && ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \ && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \ && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \ && ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
# Allow www-data write permissions where necessary && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord

View File

@ -79,14 +79,15 @@ RUN chown root:www-data ./ \
&& chmod 750 ./ \ && chmod 750 ./ \
# Files should not have execute set, but directories need it # Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \ && find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories # Create necessary directories
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \ && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, and avatars # Symlinks for env, database, and avatars
&& ln -s /pelican-data/.env ./.env \ && ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \ && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \ && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \ && ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
# Allow www-data write permissions where necessary && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord

View File

@ -18,6 +18,17 @@ class QueueWorkerServiceCommand extends Command
public function handle(): void public function handle(): void
{ {
if (@file_exists('/.dockerenv')) {
$result = Process::run('supervisorctl restart queue-worker');
if ($result->failed()) {
$this->error('Error restarting service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file updated successfully.');
return;
}
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue'); $serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service'; $path = '/etc/systemd/system/' . $serviceName . '.service';

View File

@ -24,6 +24,7 @@ class MakeNodeCommand extends Command
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).} {--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
{--uploadSize= : Enter the maximum upload filesize.} {--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.} {--daemonListeningPort= : Enter the daemon listening port.}
{--daemonConnectingPort= : Enter the daemon connecting port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.} {--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.} {--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}'; {--daemonBase= : Enter the base folder.}';
@ -57,6 +58,7 @@ class MakeNodeCommand extends Command
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1'); $data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1');
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256'); $data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080'); $data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080');
$data['daemon_connect'] = $this->option('daemonConnectingPort') ?? $this->ask(trans('commands.make_node.daemonConnect'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022'); $data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), ''); $data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes'); $data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');

View File

@ -7,7 +7,6 @@ use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand; use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand; use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use App\Models\Webhook; use App\Models\Webhook;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
@ -31,8 +30,11 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags if (config('cache.default') === 'redis') {
$schedule->command('cache:prune-stale-tags')->hourly(); // https://laravel.com/docs/10.x/upgrade#redis-cache-tags
// This only needs to run when using redis. anything else throws an error.
$schedule->command('cache:prune-stale-tags')->hourly();
}
// Execute scheduled commands for servers every minute, as if there was a normal cron running. // Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping(); $schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
@ -41,8 +43,6 @@ class Kernel extends ConsoleKernel
$schedule->command(PruneImagesCommand::class)->daily(); $schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly(); $schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
if (config('backups.prune_age')) { if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted. // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes(); $schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();

View File

@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function isStartable(): bool public function isStartable(): bool
{ {
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]); return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]);
} }
public function isRestartable(): bool public function isRestartable(): bool
@ -97,18 +97,16 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
return true; return true;
} }
return !in_array($this, [ContainerStatus::Offline]); return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
} }
public function isStoppable(): bool public function isStoppable(): bool
{ {
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]); return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]);
} }
public function isKillable(): bool public function isKillable(): bool
{ {
// [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created] return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]);
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
} }
} }

View File

@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum HeaderActionPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum HeaderWidgetPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

@ -27,8 +27,16 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
}; };
} }
public function getColor(): ?string public function getColor(bool $hex = false): string
{ {
if ($hex) {
return match ($this) {
self::Normal, self::Installing, self::RestoringBackup => '#2563EB',
self::Suspended => '#D97706',
self::InstallFailed, self::ReinstallFailed => '#EF4444',
};
}
return match ($this) { return match ($this) {
self::Normal => 'primary', self::Normal => 'primary',
self::Installing => 'primary', self::Installing => 'primary',

View File

@ -1,11 +0,0 @@
<?php
namespace App\Events\Auth;
use App\Models\User;
use App\Events\Event;
class DirectLogin extends Event
{
public function __construct(public User $user, public bool $remember) {}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Events\Auth;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class FailedPasswordReset extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public string $email) {}
}

View File

@ -8,13 +8,16 @@ use App\Models\Server;
use App\Models\ServerVariable; use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonPowerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Closure; use Closure;
use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
class GSLToken extends FeatureProvider class GSLToken extends FeatureProvider
{ {
@ -27,14 +30,14 @@ class GSLToken extends FeatureProvider
public function getListeners(): array public function getListeners(): array
{ {
return [ return [
'gsl token expired', '(gsl token expired)',
'account not found', '(account not found)',
]; ];
} }
public function getId(): string public function getId(): string
{ {
return 'gsltoken'; return 'gsl_token';
} }
/** /**
@ -46,18 +49,19 @@ class GSLToken extends FeatureProvider
$server = Filament::getTenant(); $server = Filament::getTenant();
/** @var ServerVariable $serverVariable */ /** @var ServerVariable $serverVariable */
$serverVariable = $server->serverVariables()->where('env_variable', 'STEAM_ACC')->first(); $serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
$query->where('env_variable', 'STEAM_ACC');
})->first();
return Action::make($this->getId()) return Action::make($this->getId())
->requiresConfirmation() ->requiresConfirmation()
->modalHeading('Invalid GSL token') ->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.') ->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token') ->modalSubmitActionLabel('Update GSL Token')
->disabledSchema(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->schema([ ->form([
TextEntry::make('java') Placeholder::make('info')
->label('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it ->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
completely.'),
TextInput::make('gsltoken') TextInput::make('gsltoken')
->label('GSL Token') ->label('GSL Token')
->rules([ ->rules([
@ -105,13 +109,13 @@ class GSLToken extends FeatureProvider
Notification::make() Notification::make()
->title('GSL Token updated') ->title('GSL Token updated')
->body('Restart the server to use the new token.') ->body('Server will restart now.')
->success() ->success()
->send(); ->send();
} catch (\Exception $e) { } catch (Exception $exception) {
Notification::make() Notification::make()
->title('Error') ->title('Could not update GSL Token')
->body($e->getMessage()) ->body($exception->getMessage())
->danger() ->danger()
->send(); ->send();
} }

View File

@ -6,6 +6,7 @@ use App\Facades\Activity;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Placeholder;
@ -25,10 +26,11 @@ class JavaVersion extends FeatureProvider
{ {
return [ return [
'java.lang.UnsupportedClassVersionError', 'java.lang.UnsupportedClassVersionError',
'minecraft 1.17 requires running the server with java 16 or above',
'minecraft 1.18 requires running the server with java 17 or above',
'unsupported major.minor version', 'unsupported major.minor version',
'has been compiled by a more recent version of the java runtime', 'has been compiled by a more recent version of the java runtime',
'minecraft 1.17 requires running the server with java 16 or above',
'minecraft 1.18 requires running the server with java 17 or above',
'minecraft 1.19 requires running the server with java 17 or above',
]; ];
} }
@ -73,17 +75,18 @@ class JavaVersion extends FeatureProvider
->property(['old' => $original, 'new' => $new]) ->property(['old' => $original, 'new' => $new])
->log(); ->log();
} }
$powerRepository->setServer($server)->send('restart'); $powerRepository->setServer($server)->send('restart');
Notification::make() Notification::make()
->title('Docker image updated') ->title('Docker image updated')
->body('Restart the server to use the new image.') ->body('Server will restart now.')
->success() ->success()
->send(); ->send();
} catch (\Exception $e) { } catch (Exception $exception) {
Notification::make() Notification::make()
->title('Error') ->title('Could not update docker image')
->body($e->getMessage()) ->body($exception->getMessage())
->danger() ->danger()
->send(); ->send();
} }

View File

@ -24,7 +24,7 @@ class MinecraftEula extends FeatureProvider
public function getListeners(): array public function getListeners(): array
{ {
return [ return [
'You need to agree to the EULA in order to run the server', 'you need to agree to the eula in order to run the server',
]; ];
} }
@ -38,31 +38,30 @@ class MinecraftEula extends FeatureProvider
return Action::make($this->getId()) return Action::make($this->getId())
->requiresConfirmation() ->requiresConfirmation()
->modalHeading('Minecraft EULA') ->modalHeading('Minecraft EULA')
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>'))) ->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
->modalSubmitActionLabel('I Accept') ->modalSubmitActionLabel('I Accept')
->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) { ->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
try { try {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$content = $fileRepository->setServer($server)->getContent('eula.txt');
$content = preg_replace('/(eula=)false/', '\1true', $content); $fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
$fileRepository->setServer($server)->putContent('eula.txt', $content);
$powerRepository->setServer($server)->send('restart'); $powerRepository->setServer($server)->send('restart');
Notification::make() Notification::make()
->title('Docker image updated') ->title('Minecraft EULA accepted')
->body('Restart the server.') ->body('Server will restart now.')
->success() ->success()
->send(); ->send();
} catch (Exception $e) { } catch (Exception $exception) {
Notification::make() Notification::make()
->title('Error') ->title('Could not accept Minecraft EULA')
->body($e->getMessage()) ->body($exception->getMessage())
->danger() ->danger()
->send(); ->send();
} }
} });
);
} }
public static function register(Application $app): self public static function register(Application $app): self

View File

@ -8,9 +8,15 @@ use App\Extensions\OAuth\Providers\OAuthProvider;
use App\Models\Backup; use App\Models\Backup;
use App\Notifications\MailTested; use App\Notifications\MailTested;
use App\Traits\EnvironmentWriterTrait; use App\Traits\EnvironmentWriterTrait;
use BackedEnum; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
@ -31,8 +37,8 @@ use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;
use Filament\Support\Enums\Width;
use Illuminate\Http\Client\Factory; use Illuminate\Http\Client\Factory;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification; use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -43,9 +49,12 @@ use Filament\Schemas\Contracts\HasSchemas;
*/ */
class Settings extends Page implements HasSchemas class Settings extends Page implements HasSchemas
{ {
use CanCustomizeHeaderActions, InteractsWithHeaderActions {
CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions;
}
use CanCustomizeHeaderWidgets;
use EnvironmentWriterTrait; use EnvironmentWriterTrait;
use InteractsWithForms; use InteractsWithForms;
use InteractsWithHeaderActions;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-settings'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-settings';
@ -138,8 +147,7 @@ class Settings extends Page implements HasSchemas
->placeholder('/pelican.ico'), ->placeholder('/pelican.ico'),
]), ]),
Group::make() Group::make()
->columnSpan(2) ->columns(2)
->columns(4)
->schema([ ->schema([
Toggle::make('APP_DEBUG') Toggle::make('APP_DEBUG')
->label(trans('admin/setting.general.debug_mode')) ->label(trans('admin/setting.general.debug_mode'))
@ -159,6 +167,10 @@ class Settings extends Page implements HasSchemas
]) ])
->stateCast(new BooleanStateCast(false, true)) ->stateCast(new BooleanStateCast(false, true))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))), ->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
]),
Group::make()
->columns(2)
->schema([
Select::make('FILAMENT_AVATAR_PROVIDER') Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider')) ->label(trans('admin/setting.general.avatar_provider'))
->native(false) ->native(false)
@ -195,12 +207,18 @@ class Settings extends Page implements HasSchemas
->formatStateUsing(fn ($state): int => (int) $state) ->formatStateUsing(fn ($state): int => (int) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state)) ->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))), ->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
Select::make('FILAMENT_WIDTH')
->label(trans('admin/setting.general.display_width'))
->native(false)
->options(MaxWidth::class)
->selectablePlaceholder(false)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
TagsInput::make('TRUSTED_PROXIES') TagsInput::make('TRUSTED_PROXIES')
->label(trans('admin/setting.general.trusted_proxies')) ->label(trans('admin/setting.general.trusted_proxies'))
->separator() ->separator()
->splitKeys(['Tab', ' ']) ->splitKeys(['Tab', ' '])
->placeholder(trans('admin/setting.general.trusted_proxies_help')) ->placeholder(trans('admin/setting.general.trusted_proxies_help'))
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies')))) ->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies')))))
->hintActions([ ->hintActions([
Action::make('clear') Action::make('clear')
->label(trans('admin/setting.general.clear')) ->label(trans('admin/setting.general.clear'))
@ -235,12 +253,6 @@ class Settings extends Page implements HasSchemas
$set('TRUSTED_PROXIES', $ips->values()->all()); $set('TRUSTED_PROXIES', $ips->values()->all());
}), }),
]), ]),
Select::make('FILAMENT_WIDTH')
->label(trans('admin/setting.general.display_width'))
->native(false)
->options(Width::class)
->selectablePlaceholder(false)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
]; ];
} }
@ -635,8 +647,8 @@ class Settings extends Page implements HasSchemas
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->columnSpanFull() ->formatStateUsing(fn ($state): bool => (bool) $state)
->stateCast(new BooleanStateCast(false)) ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))), ->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION') Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.server_reinstalled')) ->label(trans('admin/setting.misc.mail_notifications.server_reinstalled'))
@ -645,8 +657,8 @@ class Settings extends Page implements HasSchemas
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->columnSpanFull() ->formatStateUsing(fn ($state): bool => (bool) $state)
->stateCast(new BooleanStateCast(false)) ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))), ->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
]), ]),
Section::make(trans('admin/setting.misc.connections.title')) Section::make(trans('admin/setting.misc.connections.title'))
@ -731,9 +743,17 @@ class Settings extends Page implements HasSchemas
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->columnSpanFull() ->columnSpan(1)
->stateCast(new BooleanStateCast(false)) ->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))), ->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
FileUpload::make('ConsoleFonts')
->hint(trans('admin/setting.misc.server.console_font_hint'))
->label(trans('admin/setting.misc.server.console_font_upload'))
->directory('fonts')
->columnSpan(1)
->maxFiles(1)
->preserveFilenames(),
]), ]),
Section::make(trans('admin/setting.misc.webhook.title')) Section::make(trans('admin/setting.misc.webhook.title'))
->description(trans('admin/setting.misc.webhook.helper')) ->description(trans('admin/setting.misc.webhook.helper'))
@ -762,6 +782,7 @@ class Settings extends Page implements HasSchemas
{ {
try { try {
$data = $this->form->getState(); $data = $this->form->getState();
unset($data['ConsoleFonts']);
$data = array_map(function ($value) { $data = array_map(function ($value) {
// Convert bools to a string, so they are correctly written to the .env file // Convert bools to a string, so they are correctly written to the .env file
@ -797,7 +818,8 @@ class Settings extends Page implements HasSchemas
} }
} }
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Action::make('save') Action::make('save')

View File

@ -6,11 +6,17 @@ use App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser; use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Exception;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Form; use Filament\Schemas\Components\Form;
@ -20,6 +26,11 @@ use Illuminate\Database\Eloquent\Builder;
class ApiKeyResource extends Resource class ApiKeyResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = ApiKey::class; protected static ?string $model = ApiKey::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-key'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-key';
@ -56,7 +67,10 @@ class ApiKeyResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
public static function table(Table $table): Table /**
* @throws Exception
*/
public static function defaultTable(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -79,7 +93,7 @@ class ApiKeyResource extends Resource
TextColumn::make('user.username') TextColumn::make('user.username')
->label(trans('admin/apikey.table.created_by')) ->label(trans('admin/apikey.table.created_by'))
->icon('tabler-user') ->icon('tabler-user')
->url(fn (ApiKey $apiKey) => auth()->user()->can('update user', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null), ->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
]) ])
->actions([ ->actions([
DeleteAction::make(), DeleteAction::make(),
@ -92,9 +106,12 @@ class ApiKeyResource extends Resource
]); ]);
} }
public static function form(Form|\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema /**
* @throws Exception
*/
public static function form(Form|\Filament\Schemas\Schema $form): \Filament\Schemas\Schema
{ {
return $schema return $form
->schema([ ->schema([
Fieldset::make('Permissions') Fieldset::make('Permissions')
->columns([ ->columns([
@ -142,7 +159,8 @@ class ApiKeyResource extends Resource
]); ]);
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListApiKeys::route('/'), 'index' => Pages\ListApiKeys::route('/'),

View File

@ -4,16 +4,24 @@ namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Admin\Resources\ApiKeyResource; use App\Filament\Admin\Resources\ApiKeyResource;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class CreateApiKey extends CreateRecord class CreateApiKey extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ApiKeyResource::class; protected static string $resource = ApiKeyResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Admin\Resources\ApiKeyResource; use App\Filament\Admin\Resources\ApiKeyResource;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListApiKeys extends ListRecords class ListApiKeys extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ApiKeyResource::class; protected static string $resource = ApiKeyResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -3,22 +3,35 @@
namespace App\Filament\Admin\Resources; namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\DatabaseHostResource\Pages; use App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Exception;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Actions\ViewAction; use Filament\Actions\ViewAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class DatabaseHostResource extends Resource class DatabaseHostResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = DatabaseHost::class; protected static ?string $model = DatabaseHost::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-database'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-database';
@ -27,7 +40,7 @@ class DatabaseHostResource extends Resource
public static function getNavigationBadge(): ?string public static function getNavigationBadge(): ?string
{ {
return static::getModel()::count() ?: null; return (string) static::getEloquentQuery()->count() ?: null;
} }
public static function getNavigationLabel(): string public static function getNavigationLabel(): string
@ -50,7 +63,10 @@ class DatabaseHostResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
public static function table(Table $table): Table /**
* @throws Exception
*/
public static function defaultTable(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -88,9 +104,12 @@ class DatabaseHostResource extends Resource
]); ]);
} }
public static function form(Schema $schema): Schema /**
* @throws Exception
*/
public static function form(Schema $form): Schema
{ {
return $schema return $form
->components([ ->components([
Section::make() Section::make()
->columnSpanFull() ->columnSpanFull()
@ -145,12 +164,21 @@ class DatabaseHostResource extends Resource
->preload() ->preload()
->helperText(trans('admin/databasehost.linked_nodes_help')) ->helperText(trans('admin/databasehost.linked_nodes_help'))
->label(trans('admin/databasehost.linked_nodes')) ->label(trans('admin/databasehost.linked_nodes'))
->relationship('nodes', 'name'), ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
]), ]),
]); ]);
} }
public static function getPages(): array /** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
RelationManagers\DatabasesRelationManager::class,
];
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListDatabaseHosts::route('/'), 'index' => Pages\ListDatabaseHosts::route('/'),
@ -159,4 +187,15 @@ class DatabaseHostResource extends Resource
'edit' => Pages\EditDatabaseHost::route('/{record}/edit'), 'edit' => Pages\EditDatabaseHost::route('/{record}/edit'),
]; ];
} }
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
return $query->where(function (Builder $query) {
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
});
}
} }

View File

@ -4,8 +4,9 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource; use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService; use App\Services\Databases\Hosts\HostCreationService;
use Exception; use App\Traits\Filament\CanCustomizeHeaderActions;
use Filament\Schemas\Components\Fieldset; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
@ -18,6 +19,7 @@ use Filament\Resources\Pages\CreateRecord;
use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard; use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard;
use Filament\Schemas\Components\Wizard\Step; use Filament\Schemas\Components\Wizard\Step;
use Filament\Support\Exceptions\Halt; use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -26,6 +28,8 @@ use Throwable;
class CreateDatabaseHost extends CreateRecord class CreateDatabaseHost extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use HasWizard; use HasWizard;
protected static string $resource = DatabaseHostResource::class; protected static string $resource = DatabaseHostResource::class;
@ -148,7 +152,7 @@ class CreateDatabaseHost extends CreateRecord
->preload() ->preload()
->helperText(trans('admin/databasehost.linked_nodes_help')) ->helperText(trans('admin/databasehost.linked_nodes_help'))
->label(trans('admin/databasehost.linked_nodes')) ->label(trans('admin/databasehost.linked_nodes'))
->relationship('nodes', 'name'), ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
]), ]),
]; ];
} }

View File

@ -3,19 +3,24 @@
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource; use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Services\Databases\Hosts\HostUpdateService; use App\Services\Databases\Hosts\HostUpdateService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Filament\Support\Exceptions\Halt; use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use PDOException; use PDOException;
use Throwable;
class EditDatabaseHost extends EditRecord class EditDatabaseHost extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseHostResource::class; protected static string $resource = DatabaseHostResource::class;
private HostUpdateService $hostUpdateService; private HostUpdateService $hostUpdateService;
@ -25,7 +30,8 @@ class EditDatabaseHost extends EditRecord
$this->hostUpdateService = $hostUpdateService; $this->hostUpdateService = $hostUpdateService;
} }
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()
@ -40,21 +46,6 @@ class EditDatabaseHost extends EditRecord
return []; return [];
} }
public function getRelationManagers(): array
{
if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) {
return [
DatabasesRelationManager::class,
];
}
return [];
}
/**
* @throws Halt
* @throws Throwable
*/
protected function handleRecordUpdate(Model $record, array $data): Model protected function handleRecordUpdate(Model $record, array $data): Model
{ {
if (!$record instanceof DatabaseHost) { if (!$record instanceof DatabaseHost) {

View File

@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource; use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListDatabaseHosts extends ListRecords class ListDatabaseHosts extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseHostResource::class; protected static string $resource = DatabaseHostResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -3,29 +3,25 @@
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource; use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord; use Filament\Resources\Pages\ViewRecord;
class ViewDatabaseHost extends ViewRecord class ViewDatabaseHost extends ViewRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseHostResource::class; protected static string $resource = DatabaseHostResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),
]; ];
} }
public function getRelationManagers(): array
{
if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) {
return [
DatabasesRelationManager::class,
];
}
return [];
}
} }

View File

@ -68,11 +68,11 @@ class DatabasesRelationManager extends RelationManager
->label(trans('admin/databasehost.table.created_at')), ->label(trans('admin/databasehost.table.created_at')),
]) ])
->actions([ ->actions([
\Filament\Actions\DeleteAction::make() DeleteAction::make()
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)), ->authorize(fn (Database $database) => auth()->user()->can('delete', $database)),
\Filament\Actions\ViewAction::make() ViewAction::make()
->color('primary') ->color('primary')
->hidden(fn () => !auth()->user()->can('viewList database')), ->hidden(fn () => !auth()->user()->can('viewAny', Database::class)),
]); ]);
} }
} }

View File

@ -3,11 +3,19 @@
namespace App\Filament\Admin\Resources; namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\EggResource\Pages; use App\Filament\Admin\Resources\EggResource\Pages;
use App\Filament\Admin\Resources\EggResource\RelationManagers;
use App\Models\Egg; use App\Models\Egg;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource; use Filament\Resources\Resource;
class EggResource extends Resource class EggResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
protected static ?string $model = Egg::class; protected static ?string $model = Egg::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-eggs'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-eggs';
@ -44,7 +52,16 @@ class EggResource extends Resource
return ['name', 'tags', 'uuid', 'id']; return ['name', 'tags', 'uuid', 'id'];
} }
public static function getPages(): array /** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
RelationManagers\ServersRelationManager::class,
];
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListEggs::route('/'), 'index' => Pages\ListEggs::route('/'),

View File

@ -5,6 +5,10 @@ namespace App\Filament\Admin\Resources\EggResource\Pages;
use App\Filament\Admin\Resources\EggResource; use App\Filament\Admin\Resources\EggResource;
use App\Filament\Components\Forms\Fields\CopyFrom; use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Models\EggVariable; use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CodeEditor; use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
@ -28,11 +32,15 @@ use Filament\Schemas\Schema;
class CreateEgg extends CreateRecord class CreateEgg extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = EggResource::class; protected static string $resource = EggResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -3,12 +3,15 @@
namespace App\Filament\Admin\Resources\EggResource\Pages; namespace App\Filament\Admin\Resources\EggResource\Pages;
use App\Filament\Admin\Resources\EggResource; use App\Filament\Admin\Resources\EggResource;
use App\Filament\Admin\Resources\EggResource\RelationManagers\ServersRelationManager;
use App\Filament\Components\Actions\ExportEggAction; use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction; use App\Filament\Components\Actions\ImportEggAction;
use App\Filament\Components\Forms\Fields\CopyFrom; use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Models\Egg; use App\Models\Egg;
use App\Models\EggVariable; use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CodeEditor; use Filament\Forms\Components\CodeEditor;
@ -31,6 +34,9 @@ use Filament\Schemas\Schema;
class EditEgg extends EditRecord class EditEgg extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = EggResource::class; protected static string $resource = EggResource::class;
/** /**
@ -250,7 +256,8 @@ class EditEgg extends EditRecord
]); ]);
} }
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()
@ -272,11 +279,4 @@ class EditEgg extends EditRecord
{ {
return []; return [];
} }
public function getRelationManagers(): array
{
return [
ServersRelationManager::class,
];
}
} }

View File

@ -10,7 +10,10 @@ use App\Filament\Components\Tables\Actions\UpdateEggAction;
use App\Filament\Components\Tables\Actions\UpdateEggBulkAction; use App\Filament\Components\Tables\Actions\UpdateEggBulkAction;
use App\Filament\Components\Tables\Filters\TagsFilter; use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Egg; use App\Models\Egg;
use Filament\Actions\CreateAction; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction as CreateHeaderAction; use Filament\Actions\CreateAction as CreateHeaderAction;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
@ -23,6 +26,9 @@ use Illuminate\Support\Str;
class ListEggs extends ListRecords class ListEggs extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = EggResource::class; protected static string $resource = EggResource::class;
public function table(Table $table): Table public function table(Table $table): Table
@ -95,7 +101,8 @@ class ListEggs extends ListRecords
]); ]);
} }
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
ImportEggHeaderAction::make() ImportEggHeaderAction::make()

View File

@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\MountResource\Pages; use App\Filament\Admin\Resources\MountResource\Pages;
use App\Models\Mount; use App\Models\Mount;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
@ -12,15 +16,22 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Components\Form;
use Filament\Schemas\Components\Group; use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class MountResource extends Resource class MountResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = Mount::class; protected static ?string $model = Mount::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-layers-linked'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-layers-linked';
@ -44,7 +55,7 @@ class MountResource extends Resource
public static function getNavigationBadge(): ?string public static function getNavigationBadge(): ?string
{ {
return static::getModel()::count() ?: null; return (string) static::getEloquentQuery()->count() ?: null;
} }
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
@ -52,7 +63,10 @@ class MountResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
public static function table(Table $table): Table /**
* @throws \Exception
*/
public static function defaultTable(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -75,7 +89,7 @@ class MountResource extends Resource
->badge() ->badge()
->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing') ->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing')
->color(fn ($state) => $state ? 'success' : 'warning') ->color(fn ($state) => $state ? 'success' : 'warning')
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writeable')), ->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
]) ])
->actions([ ->actions([
ViewAction::make() ViewAction::make()
@ -93,9 +107,12 @@ class MountResource extends Resource
]); ]);
} }
public static function form(Form|\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema /**
* @throws \Exception
*/
public static function form(Schema $form): Schema
{ {
return $schema return $form
->schema([ ->schema([
Section::make()->schema([ Section::make()->schema([
TextInput::make('name') TextInput::make('name')
@ -147,7 +164,7 @@ class MountResource extends Resource
->preload(), ->preload(),
Select::make('nodes')->multiple() Select::make('nodes')->multiple()
->label(trans('admin/mount.nodes')) ->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name') ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')))
->searchable(['name', 'fqdn']) ->searchable(['name', 'fqdn'])
->preload(), ->preload(),
]), ]),
@ -161,7 +178,8 @@ class MountResource extends Resource
]); ]);
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListMounts::route('/'), 'index' => Pages\ListMounts::route('/'),
@ -170,4 +188,15 @@ class MountResource extends Resource
'edit' => Pages\EditMount::route('/{record}/edit'), 'edit' => Pages\EditMount::route('/{record}/edit'),
]; ];
} }
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
return $query->where(function (Builder $query) {
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
});
}
} }

View File

@ -3,17 +3,25 @@
namespace App\Filament\Admin\Resources\MountResource\Pages; namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource; use App\Filament\Admin\Resources\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class CreateMount extends CreateRecord class CreateMount extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class; protected static string $resource = MountResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\MountResource\Pages; namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource; use App\Filament\Admin\Resources\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditMount extends EditRecord class EditMount extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class; protected static string $resource = MountResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make(), DeleteAction::make(),

View File

@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource; use App\Filament\Admin\Resources\MountResource;
use App\Models\Mount; use App\Models\Mount;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListMounts extends ListRecords class ListMounts extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class; protected static string $resource = MountResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\MountResource\Pages; namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource; use App\Filament\Admin\Resources\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord; use Filament\Resources\Pages\ViewRecord;
class ViewMount extends ViewRecord class ViewMount extends ViewRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class; protected static string $resource = MountResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),

View File

@ -5,10 +5,18 @@ namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\NodeResource\Pages; use App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Admin\Resources\NodeResource\RelationManagers; use App\Filament\Admin\Resources\NodeResource\RelationManagers;
use App\Models\Node; use App\Models\Node;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class NodeResource extends Resource class NodeResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
protected static ?string $model = Node::class; protected static ?string $model = Node::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-server-2'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-server-2';
@ -37,10 +45,11 @@ class NodeResource extends Resource
public static function getNavigationBadge(): ?string public static function getNavigationBadge(): ?string
{ {
return static::getModel()::count() ?: null; return (string) static::getEloquentQuery()->count() ?: null;
} }
public static function getRelations(): array /** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{ {
return [ return [
RelationManagers\AllocationsRelationManager::class, RelationManagers\AllocationsRelationManager::class,
@ -48,7 +57,8 @@ class NodeResource extends Resource
]; ];
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListNodes::route('/'), 'index' => Pages\ListNodes::route('/'),
@ -56,4 +66,11 @@ class NodeResource extends Resource
'edit' => Pages\EditNode::route('/{record}/edit'), 'edit' => Pages\EditNode::route('/{record}/edit'),
]; ];
} }
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
return $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
}
} }

View File

@ -4,9 +4,11 @@ namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Admin\Resources\NodeResource; use App\Filament\Admin\Resources\NodeResource;
use App\Models\Node; use App\Models\Node;
use Filament\Actions\Action; use App\Traits\Filament\CanCustomizeHeaderActions;
use Filament\Schemas\Components\Form; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Schemas\Components\Grid; use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
@ -21,6 +23,9 @@ use Filament\Schemas\Schema;
class CreateNode extends CreateRecord class CreateNode extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = NodeResource::class; protected static string $resource = NodeResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
@ -124,15 +129,10 @@ class CreateNode extends CreateRecord
'lg' => 1, 'lg' => 1,
]), ]),
TextInput::make('daemon_listen') TextInput::make('daemon_connect')
->columnSpan([ ->columnSpan(1)
'default' => 1, ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
'sm' => 1, ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
'md' => 1,
'lg' => 1,
])
->label(trans('admin/node.port'))
->helperText(trans('admin/node.port_help'))
->minValue(1) ->minValue(1)
->maxValue(65535) ->maxValue(65535)
->default(8080) ->default(8080)
@ -150,14 +150,15 @@ class CreateNode extends CreateRecord
->required() ->required()
->maxLength(100), ->maxLength(100),
ToggleButtons::make('scheme') Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection')
->label(trans('admin/node.ssl')) ->label(trans('admin/node.ssl'))
->columnSpan([ ->columnSpan(1)
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->inline() ->inline()
->helperText(function (Get $get) { ->helperText(function (Get $get) {
if (request()->isSecure()) { if (request()->isSecure()) {
@ -170,20 +171,43 @@ class CreateNode extends CreateRecord
return ''; return '';
}) })
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure()) ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([ ->options([
'http' => 'HTTP', 'http' => 'HTTP',
'https' => 'HTTPS (SSL)', 'https' => 'HTTPS (SSL)',
'https_proxy' => 'HTTPS with (reverse) proxy',
]) ])
->colors([ ->colors([
'http' => 'warning', 'http' => 'warning',
'https' => 'success', 'https' => 'success',
'https_proxy' => 'success',
]) ])
->icons([ ->icons([
'http' => 'tabler-lock-open-off', 'http' => 'tabler-lock-open-off',
'https' => 'tabler-lock', 'https' => 'tabler-lock',
'https_proxy' => 'tabler-shield-lock',
]) ])
->default(fn () => request()->isSecure() ? 'https' : 'http'), ->default(fn () => request()->isSecure() ? 'https' : 'http')
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]), ]),
Step::make('advanced') Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings')) ->label(trans('admin/node.tabs.advanced_settings'))
@ -399,4 +423,13 @@ class CreateNode extends CreateRecord
{ {
return []; return [];
} }
protected function mutateFormDataBeforeCreate(array $data): array
{
if (!$data['behind_proxy']) {
$data['daemon_listen'] = $data['daemon_connect'];
}
return $data;
}
} }

View File

@ -8,12 +8,15 @@ use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Services\Helpers\SoftwareVersionService; use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService; use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService; use App\Services\Nodes\NodeUpdateService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions;
use Filament\Actions\DeleteAction; use Filament\Forms\Components\Fieldset;
use Filament\Schemas\Components\Actions; use Filament\Forms\Components\Grid;
use Filament\Schemas\Components\Fieldset; use Filament\Forms\Components\Hidden;
use Filament\Infolists\Components\TextEntry; use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
@ -35,6 +38,9 @@ use Illuminate\Support\HtmlString;
class EditNode extends EditRecord class EditNode extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = NodeResource::class; protected static string $resource = NodeResource::class;
private DaemonConfigurationRepository $daemonConfigurationRepository; private DaemonConfigurationRepository $daemonConfigurationRepository;
@ -184,10 +190,10 @@ class EditNode extends EditRecord
0 => 'danger', 0 => 'danger',
]) ])
->columnSpan(1), ->columnSpan(1),
TextInput::make('daemon_listen') TextInput::make('daemon_connect')
->columnSpan(1) ->columnSpan(1)
->label(trans('admin/node.port')) ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
->helperText(trans('admin/node.port_help')) ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
->minValue(1) ->minValue(1)
->maxValue(65535) ->maxValue(65535)
->default(8080) ->default(8080)
@ -203,7 +209,9 @@ class EditNode extends EditRecord
]) ])
->required() ->required()
->maxLength(100), ->maxLength(100),
ToggleButtons::make('scheme') Hidden::make('scheme'),
Hidden::make('behind_proxy'),
ToggleButtons::make('connection')
->label(trans('admin/node.ssl')) ->label(trans('admin/node.ssl'))
->columnSpan(1) ->columnSpan(1)
->inline() ->inline()
@ -218,20 +226,43 @@ class EditNode extends EditRecord
return ''; return '';
}) })
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure()) ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([ ->options([
'http' => 'HTTP', 'http' => 'HTTP',
'https' => 'HTTPS (SSL)', 'https' => 'HTTPS (SSL)',
'https_proxy' => 'HTTPS with (reverse) proxy',
]) ])
->colors([ ->colors([
'http' => 'warning', 'http' => 'warning',
'https' => 'success', 'https' => 'success',
'https_proxy' => 'success',
]) ])
->icons([ ->icons([
'http' => 'tabler-lock-open-off', 'http' => 'tabler-lock-open-off',
'https' => 'tabler-lock', 'https' => 'tabler-lock',
'https_proxy' => 'tabler-shield-lock',
]) ])
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]), ->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https'))
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Tab::make('adv') Tab::make('adv')
->label(trans('admin/node.tabs.advanced_settings')) ->label(trans('admin/node.tabs.advanced_settings'))
->columns([ ->columns([
@ -618,7 +649,8 @@ class EditNode extends EditRecord
return []; return [];
} }
protected function getHeaderActions(): array /** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()
@ -628,6 +660,15 @@ class EditNode extends EditRecord
]; ];
} }
protected function mutateFormDataBeforeSave(array $data): array
{
if (!$data['behind_proxy']) {
$data['daemon_listen'] = $data['daemon_connect'];
}
return $data;
}
protected function afterSave(): void protected function afterSave(): void
{ {
$this->fillForm(); $this->fillForm();

View File

@ -6,6 +6,9 @@ use App\Filament\Admin\Resources\NodeResource;
use App\Filament\Components\Tables\Columns\NodeHealthColumn; use App\Filament\Components\Tables\Columns\NodeHealthColumn;
use App\Filament\Components\Tables\Filters\TagsFilter; use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Node; use App\Models\Node;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
@ -15,6 +18,9 @@ use Filament\Tables\Table;
class ListNodes extends ListRecords class ListNodes extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = NodeResource::class; protected static string $resource = NodeResource::class;
public function table(Table $table): Table public function table(Table $table): Table
@ -72,7 +78,8 @@ class ListNodes extends ListRecords
]); ]);
} }
protected function getHeaderActions(): array /** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -13,7 +13,6 @@ use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\SelectColumn; use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
@ -32,18 +31,12 @@ class AllocationsRelationManager extends RelationManager
public function setTitle(): string public function setTitle(): string
{ {
return trans('admin/server.allocations'); return trans('admin/server.allocations');
} }
public function table(Table $table): Table public function table(Table $table): Table
{ {
return $table return $table
->recordTitleAttribute('ip') ->recordTitleAttribute('address')
// Non Primary Allocations
// ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
// All assigned allocations
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null) ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->paginationPageOptions(['10', '20', '50', '100', '200', '500']) ->paginationPageOptions(['10', '20', '50', '100', '200', '500'])
->searchable() ->searchable()
@ -79,7 +72,7 @@ class AllocationsRelationManager extends RelationManager
->options(collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip])) ->options(collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label(trans('admin/node.ip_address')) ->label(trans('admin/node.ip_address'))
->inlineLabel() ->inlineLabel()
->ipv4() ->ip()
->helperText(trans('admin/node.ip_help')) ->helperText(trans('admin/node.ip_help'))
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', [])) ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->live() ->live()
@ -96,19 +89,15 @@ class AllocationsRelationManager extends RelationManager
->inlineLabel() ->inlineLabel()
->live() ->live()
->disabled(fn (Get $get) => empty($get('allocation_ip'))) ->disabled(fn (Get $get) => empty($get('allocation_ip')))
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip'))))
CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip')))
)
->splitKeys(['Tab', ' ', ',']) ->splitKeys(['Tab', ' ', ','])
->required(), ->required(),
]) ])
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)), ->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
]) ])
->bulkActions([ ->groupedBulkActions([
BulkActionGroup::make([ DeleteBulkAction::make()
DeleteBulkAction::make() ->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())),
->authorize(fn () => auth()->user()->can('update node')),
]),
]); ]);
} }
} }

View File

@ -3,7 +3,6 @@
namespace App\Filament\Admin\Resources\NodeResource\Widgets; namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node; use App\Models\Node;
use Carbon\Carbon;
use Filament\Support\RawJs; use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget; use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number; use Illuminate\Support\Number;
@ -16,22 +15,34 @@ class NodeCpuChart extends ChartWidget
public Node $node; public Node $node;
/**
* @var array<int, array{cpu: string, timestamp: string}>
*/
protected array $cpuHistory = [];
protected int $threads = 0;
protected function getData(): array protected function getData(): array
{ {
$threads = $this->node->systemInformation()['cpu_count'] ?? 0; $sessionKey = "node_stats.{$this->node->id}";
$cpu = collect(cache()->get("nodes.{$this->node->id}.cpu_percent")) $data = $this->node->statistics();
->slice(-10)
->map(fn ($value, $key) => [ $this->threads = session("{$sessionKey}.threads", $this->node->systemInformation()['cpu_count'] ?? 0);
'cpu' => round($value * $threads, 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), $this->cpuHistory = session("{$sessionKey}.cpu_history", []);
]) $this->cpuHistory[] = [
->all(); 'cpu' => round($data['cpu_percent'] * $this->threads, 2),
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
];
$this->cpuHistory = array_slice($this->cpuHistory, -60);
session()->put("{$sessionKey}.cpu_history", $this->cpuHistory);
return [ return [
'datasets' => [ 'datasets' => [
[ [
'data' => array_column($cpu, 'cpu'), 'data' => array_column($this->cpuHistory, 'cpu'),
'backgroundColor' => [ 'backgroundColor' => [
'rgba(96, 165, 250, 0.3)', 'rgba(96, 165, 250, 0.3)',
], ],
@ -39,7 +50,7 @@ class NodeCpuChart extends ChartWidget
'fill' => true, 'fill' => true,
], ],
], ],
'labels' => array_column($cpu, 'timestamp'), 'labels' => array_column($this->cpuHistory, 'timestamp'),
'locale' => auth()->user()->language ?? 'en', 'locale' => auth()->user()->language ?? 'en',
]; ];
} }
@ -69,10 +80,10 @@ class NodeCpuChart extends ChartWidget
public function getHeading(): string public function getHeading(): string
{ {
$threads = $this->node->systemInformation()['cpu_count'] ?? 0; $data = array_slice(end($this->cpuHistory), -60);
$cpu = Number::format(collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))->last() * $threads, maxPrecision: 2, locale: auth()->user()->language); $cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language);
$max = Number::format($threads * 100, locale: auth()->user()->language); $max = Number::format($this->threads * 100, locale: auth()->user()->language);
return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]); return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]);
} }

View File

@ -3,7 +3,6 @@
namespace App\Filament\Admin\Resources\NodeResource\Widgets; namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node; use App\Models\Node;
use Carbon\Carbon;
use Filament\Support\RawJs; use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget; use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number; use Illuminate\Support\Number;
@ -16,19 +15,36 @@ class NodeMemoryChart extends ChartWidget
public Node $node; public Node $node;
/**
* @var array<int, array{memory: string, timestamp: string}>
*/
protected array $memoryHistory = [];
protected int $totalMemory = 0;
protected function getData(): array protected function getData(): array
{ {
$memUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->slice(-10) $sessionKey = "node_stats.{$this->node->id}";
->map(fn ($value, $key) => [
'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2), $data = $this->node->statistics();
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
]) $this->totalMemory = session("{$sessionKey}.total_memory", $data['memory_total']);
->all();
$this->memoryHistory = session("{$sessionKey}.memory_history", []);
$this->memoryHistory[] = [
'memory' => round(config('panel.use_binary_prefix')
? $data['memory_used'] / 1024 / 1024 / 1024
: $data['memory_used'] / 1000 / 1000 / 1000, 2),
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
];
$this->memoryHistory = array_slice($this->memoryHistory, -60);
session()->put("{$sessionKey}.memory_history", $this->memoryHistory);
return [ return [
'datasets' => [ 'datasets' => [
[ [
'data' => array_column($memUsed, 'memory'), 'data' => array_column($this->memoryHistory, 'memory'),
'backgroundColor' => [ 'backgroundColor' => [
'rgba(96, 165, 250, 0.3)', 'rgba(96, 165, 250, 0.3)',
], ],
@ -36,7 +52,7 @@ class NodeMemoryChart extends ChartWidget
'fill' => true, 'fill' => true,
], ],
], ],
'labels' => array_column($memUsed, 'timestamp'), 'labels' => array_column($this->memoryHistory, 'timestamp'),
'locale' => auth()->user()->language ?? 'en', 'locale' => auth()->user()->language ?? 'en',
]; ];
} }
@ -66,16 +82,15 @@ class NodeMemoryChart extends ChartWidget
public function getHeading(): string public function getHeading(): string
{ {
$latestMemoryUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->last(); $latestMemoryUsed = array_slice(end($this->memoryHistory), -60);
$totalMemory = collect(cache()->get("nodes.{$this->node->id}.memory_total"))->last();
$used = config('panel.use_binary_prefix') $used = config('panel.use_binary_prefix')
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB' ? Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB'; : Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB';
$total = config('panel.use_binary_prefix') $total = config('panel.use_binary_prefix')
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB' ? Number::format($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB'; : Number::format($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]); return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]);
} }

View File

@ -10,10 +10,16 @@ use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Actions\ViewAction; use Filament\Actions\ViewAction;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\TextEntry;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Fieldset;
@ -26,6 +32,11 @@ use Spatie\Permission\Contracts\Permission;
class RoleResource extends Resource class RoleResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = Role::class; protected static ?string $model = Role::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-users-group'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-users-group';
@ -57,7 +68,7 @@ class RoleResource extends Resource
return static::getModel()::count() ?: null; return static::getModel()::count() ?: null;
} }
public static function table(Table $table): Table public static function defaultTable(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -69,6 +80,11 @@ class RoleResource extends Resource
->badge() ->badge()
->counts('permissions') ->counts('permissions')
->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? trans('admin/role.all') : $state), ->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? trans('admin/role.all') : $state),
TextColumn::make('nodes.name')
->icon('tabler-server-2')
->label(trans('admin/role.nodes'))
->badge()
->placeholder(trans('admin/role.all')),
TextColumn::make('users_count') TextColumn::make('users_count')
->label(trans('admin/role.users')) ->label(trans('admin/role.users'))
->counts('users') ->counts('users')
@ -94,7 +110,7 @@ class RoleResource extends Resource
/** /**
* @throws Exception * @throws Exception
*/ */
public static function form(Schema $schema): Schema public static function form(Schema $form): Schema
{ {
$permissionSections = []; $permissionSections = [];
@ -108,7 +124,7 @@ class RoleResource extends Resource
$permissionSections[] = self::makeSection($model, $options); $permissionSections[] = self::makeSection($model, $options);
} }
return $schema return $form
->columns(1) ->columns(1)
->components([ ->components([
TextInput::make('name') TextInput::make('name')
@ -128,6 +144,14 @@ class RoleResource extends Resource
->label(trans('admin/role.permissions')) ->label(trans('admin/role.permissions'))
->state(trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN])) ->state(trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]))
->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN), ->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
Select::make('nodes')
->label(trans('admin/role.nodes'))
->multiple()
->relationship('nodes', 'name')
->searchable(['name', 'fqdn'])
->preload()
->hint(trans('admin/role.nodes_hint'))
->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
]); ]);
} }
@ -138,6 +162,8 @@ class RoleResource extends Resource
*/ */
private static function makeSection(string $model, array $options): Section private static function makeSection(string $model, array $options): Section
{ {
$model = ucwords($model);
$icon = null; $icon = null;
if (class_exists('\App\Filament\Admin\Resources\\' . $model . 'Resource')) { if (class_exists('\App\Filament\Admin\Resources\\' . $model . 'Resource')) {
@ -189,7 +215,8 @@ class RoleResource extends Resource
]); ]);
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListRoles::route('/'), 'index' => Pages\ListRoles::route('/'),

View File

@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource; use App\Filament\Admin\Resources\RoleResource;
use App\Models\Role; use App\Models\Role;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -14,13 +18,17 @@ use Spatie\Permission\Models\Permission;
*/ */
class CreateRole extends CreateRecord class CreateRole extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
public Collection $permissions; public Collection $permissions;
protected static string $resource = RoleResource::class; protected static string $resource = RoleResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource; use App\Filament\Admin\Resources\RoleResource;
use App\Models\Role; use App\Models\Role;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -15,6 +19,9 @@ use Spatie\Permission\Models\Permission;
*/ */
class EditRole extends EditRecord class EditRole extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = RoleResource::class; protected static string $resource = RoleResource::class;
public Collection $permissions; public Collection $permissions;
@ -45,7 +52,8 @@ class EditRole extends EditRecord
$this->record->syncPermissions($permissionModels); $this->record->syncPermissions($permissionModels);
} }
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()

View File

@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\RoleResource\Pages; namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource; use App\Filament\Admin\Resources\RoleResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListRoles extends ListRecords class ListRoles extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = RoleResource::class; protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make(), CreateAction::make(),

View File

@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\RoleResource\Pages; namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource; use App\Filament\Admin\Resources\RoleResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord; use Filament\Resources\Pages\ViewRecord;
class ViewRole extends ViewRecord class ViewRole extends ViewRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = RoleResource::class; protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),

View File

@ -3,11 +3,23 @@
namespace App\Filament\Admin\Resources; namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\ServerResource\Pages; use App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Admin\Resources\ServerResource\RelationManagers;
use App\Models\Mount;
use App\Models\Server; use App\Models\Server;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Get;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class ServerResource extends Resource class ServerResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
protected static ?string $model = Server::class; protected static ?string $model = Server::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker';
@ -36,10 +48,42 @@ class ServerResource extends Resource
public static function getNavigationBadge(): ?string public static function getNavigationBadge(): ?string
{ {
return static::getModel()::count() ?: null; return (string) static::getEloquentQuery()->count() ?: null;
} }
public static function getPages(): array public static function getMountCheckboxList(Get $get): CheckboxList
{
$allowedMounts = Mount::all();
$node = $get('node_id');
$egg = $get('egg_id');
if ($node && $egg) {
$allowedMounts = $allowedMounts->filter(fn (Mount $mount) => ($mount->nodes->isEmpty() || $mount->nodes->contains($node)) &&
($mount->eggs->isEmpty() || $mount->eggs->contains($egg))
);
}
return CheckboxList::make('mounts')
->label('')
->relationship('mounts')
->live()
->options(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
->descriptions(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
->helperText(fn () => $allowedMounts->isEmpty() ? trans('admin/server.no_mounts') : null)
->bulkToggleable()
->columnSpanFull();
}
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
RelationManagers\AllocationsRelationManager::class,
];
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListServers::route('/'), 'index' => Pages\ListServers::route('/'),
@ -47,4 +91,11 @@ class ServerResource extends Resource
'edit' => Pages\EditServer::route('/{record}/edit'), 'edit' => Pages\EditServer::route('/{record}/edit'),
]; ];
} }
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery();
return $query->whereIn('node_id', auth()->user()->accessibleNodes()->pluck('id'));
}
} }

View File

@ -11,10 +11,11 @@ use App\Services\Allocations\AssignmentService;
use App\Services\Servers\RandomWordService; use App\Services\Servers\RandomWordService;
use App\Services\Servers\ServerCreationService; use App\Services\Servers\ServerCreationService;
use App\Services\Users\UserCreationService; use App\Services\Users\UserCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Closure; use Closure;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Fieldset;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
@ -26,7 +27,6 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Schemas\Components\Form;
use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Get;
@ -46,6 +46,9 @@ use Filament\Schemas\Schema;
class CreateServer extends CreateRecord class CreateServer extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ServerResource::class; protected static string $resource = ServerResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
@ -109,14 +112,20 @@ class CreateServer extends CreateRecord
->disabledOn('edit') ->disabledOn('edit')
->prefixIcon('tabler-server-2') ->prefixIcon('tabler-server-2')
->selectablePlaceholder(false) ->selectablePlaceholder(false)
->default(fn () => ($this->node = Node::query()->latest()->first())?->id) ->default(function () {
/** @var ?Node $latestNode */
$latestNode = auth()->user()->accessibleNodes()->latest()->first();
$this->node = $latestNode;
return $this->node?->id;
})
->columnSpan([ ->columnSpan([
'default' => 1, 'default' => 1,
'sm' => 2, 'sm' => 2,
'md' => 2, 'md' => 2,
]) ])
->live() ->live()
->relationship('node', 'name') ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
->searchable() ->searchable()
->preload() ->preload()
->afterStateUpdated(function (Set $set, $state) { ->afterStateUpdated(function (Set $set, $state) {
@ -139,6 +148,7 @@ class CreateServer extends CreateRecord
->relationship('user', 'username') ->relationship('user', 'username')
->searchable(['username', 'email']) ->searchable(['username', 'email'])
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)") ->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)")
->createOptionAction(fn (Action $action) => $action->authorize(fn () => auth()->user()->can('create', User::class)))
->createOptionForm([ ->createOptionForm([
TextInput::make('username') TextInput::make('username')
->label(trans('admin/user.username')) ->label(trans('admin/user.username'))
@ -183,10 +193,7 @@ class CreateServer extends CreateRecord
$set('allocation_additional', null); $set('allocation_additional', null);
$set('allocation_additional.needstobeastringhere.extra_allocations', null); $set('allocation_additional.needstobeastringhere.extra_allocations', null);
}) })
->getOptionLabelFromRecordUsing( ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
)
->placeholder(function (Get $get) { ->placeholder(function (Get $get) {
$node = Node::find($get('node_id')); $node = Node::find($get('node_id'));
@ -203,6 +210,7 @@ class CreateServer extends CreateRecord
->where('node_id', $get('node_id')) ->where('node_id', $get('node_id'))
->whereNull('server_id'), ->whereNull('server_id'),
) )
->createOptionAction(fn (Action $action) => $action->authorize(fn (Get $get) => auth()->user()->can('create', Node::find($get('node_id')))))
->createOptionForm(function (Get $get) { ->createOptionForm(function (Get $get) {
$getPage = $get; $getPage = $get;
@ -212,7 +220,7 @@ class CreateServer extends CreateRecord
->label(trans('admin/server.ip_address'))->inlineLabel() ->label(trans('admin/server.ip_address'))->inlineLabel()
->helperText(trans('admin/server.ip_address_helper')) ->helperText(trans('admin/server.ip_address_helper'))
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', [])) ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->ipv4() ->ip()
->live() ->live()
->required(), ->required(),
TextInput::make('allocation_alias') TextInput::make('allocation_alias')
@ -263,10 +271,7 @@ class CreateServer extends CreateRecord
->columnSpan(2) ->columnSpan(2)
->disabled(fn (Get $get) => $get('../../node_id') === null) ->disabled(fn (Get $get) => $get('../../node_id') === null)
->searchable(['ip', 'port', 'ip_alias']) ->searchable(['ip', 'port', 'ip_alias'])
->getOptionLabelFromRecordUsing( ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
)
->placeholder(trans('admin/server.select_additional')) ->placeholder(trans('admin/server.select_additional'))
->disableOptionsWhenSelectedInSiblingRepeaterItems() ->disableOptionsWhenSelectedInSiblingRepeaterItems()
->relationship( ->relationship(
@ -744,7 +749,7 @@ class CreateServer extends CreateRecord
'lg' => 4, 'lg' => 4,
]) ])
->columnSpan(6) ->columnSpan(6)
->schema([ ->schema(fn (Get $get) => [
Select::make('select_image') Select::make('select_image')
->label(trans('admin/server.image_name')) ->label(trans('admin/server.image_name'))
->live() ->live()
@ -798,14 +803,7 @@ class CreateServer extends CreateRecord
->valueLabel(trans('admin/server.description')) ->valueLabel(trans('admin/server.description'))
->columnSpanFull(), ->columnSpanFull(),
CheckboxList::make('mounts') ServerResource::getMountCheckboxList($get),
->label('Mounts')
->live()
->relationship('mounts')
->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
->descriptions(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]) ?? [])
->helperText(fn () => $this->node?->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
->columnSpanFull(),
]), ]),
]), ]),
]) ])

View File

@ -2,10 +2,9 @@
namespace App\Filament\Admin\Resources\ServerResource\Pages; namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Enums\ServerState; use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Enums\SuspendAction; use App\Enums\SuspendAction;
use App\Filament\Admin\Resources\ServerResource; use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
use App\Filament\Components\Forms\Actions\PreviewStartupAction; use App\Filament\Components\Forms\Actions\PreviewStartupAction;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction; use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Server\Pages\Console; use App\Filament\Server\Pages\Console;
@ -13,7 +12,6 @@ use App\Models\Allocation;
use App\Models\Database; use App\Models\Database;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Models\Egg; use App\Models\Egg;
use App\Models\Mount;
use App\Models\Node; use App\Models\Node;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerVariable; use App\Models\ServerVariable;
@ -27,10 +25,11 @@ use App\Services\Servers\ServerDeletionService;
use App\Services\Servers\SuspensionService; use App\Services\Servers\SuspensionService;
use App\Services\Servers\ToggleInstallService; use App\Services\Servers\ToggleInstallService;
use App\Services\Servers\TransferServerService; use App\Services\Servers\TransferServerService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Closure; use Closure;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Component; use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Fieldset;
@ -51,6 +50,7 @@ use Filament\Schemas\Components\Utilities\Set;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\StateCasts\BooleanStateCast; use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Support\Enums\Alignment;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -62,6 +62,9 @@ use Random\RandomException;
class EditServer extends EditRecord class EditServer extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ServerResource::class; protected static string $resource = ServerResource::class;
private DaemonServerRepository $daemonServerRepository; private DaemonServerRepository $daemonServerRepository;
@ -140,6 +143,51 @@ class EditServer extends EditRecord
// 'lg' => 1, // 'lg' => 1,
// ]), // ]),
ToggleButtons::make('condition')
->label(trans('admin/server.server_status'))
->formatStateUsing(fn (Server $server) => $server->condition)
->options(fn ($state) => [$state->value => $state->getLabel()])
->colors(fn ($state) => [$state->value => $state->getColor()])
->icons(fn ($state) => [$state->value => $state->getIcon()])
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->hintAction(
Action::make('view_install_log')
->label(trans('admin/server.view_install_log'))
//->visible(fn (Server $server) => $server->isFailedInstall())
->modalHeading('')
->modalSubmitAction(false)
->modalFooterActionsAlignment(Alignment::Right)
->modalCancelActionLabel(trans('filament::components/modal.actions.close.label'))
->form([
MonacoEditor::make('logs')
->hiddenLabel()
->placeholderText(trans('admin/server.no_log'))
->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) {
try {
return $serverRepository->setServer($server)->getInstallLogs();
} catch (ConnectionException) {
Notification::make()
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.log_failed'))
->color('warning')
->warning()
->send();
} catch (Exception) {
return '';
}
return '';
})
->language('shell')
->view('filament.plugins.monaco-editor-logs'),
])
),
Textarea::make('description') Textarea::make('description')
->label(trans('admin/server.description')) ->label(trans('admin/server.description'))
->columnSpanFull(), ->columnSpanFull(),
@ -178,7 +226,7 @@ class EditServer extends EditRecord
->maxLength(255), ->maxLength(255),
Select::make('node_id') Select::make('node_id')
->label(trans('admin/server.node')) ->label(trans('admin/server.node'))
->relationship('node', 'name') ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
->columnSpan([ ->columnSpan([
'default' => 2, 'default' => 2,
'sm' => 1, 'sm' => 1,
@ -667,17 +715,11 @@ class EditServer extends EditRecord
]), ]),
Tab::make(trans('admin/server.mounts')) Tab::make(trans('admin/server.mounts'))
->icon('tabler-layers-linked') ->icon('tabler-layers-linked')
->schema([ ->schema(fn (Get $get) => [
CheckboxList::make('mounts') ServerResource::getMountCheckboxList($get),
->label('')
->relationship('mounts')
->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : trans('admin/server.no_mounts'))
->columnSpanFull(),
]), ]),
Tab::make(trans('admin/server.databases')) Tab::make(trans('admin/server.databases'))
->hidden(fn () => !auth()->user()->can('viewList database')) ->hidden(fn () => !auth()->user()->can('viewAny', Database::class))
->icon('tabler-database') ->icon('tabler-database')
->columns(4) ->columns(4)
->schema([ ->schema([
@ -707,8 +749,8 @@ class EditServer extends EditRecord
->requiresConfirmation() ->requiresConfirmation()
->modalIcon('tabler-database-x') ->modalIcon('tabler-database-x')
->modalHeading(trans('admin/server.delete_db_heading')) ->modalHeading(trans('admin/server.delete_db_heading'))
->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?') ->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?') ->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')]))
->action(function (DatabaseManagementService $databaseManagementService, $record) { ->action(function (DatabaseManagementService $databaseManagementService, $record) {
$databaseManagementService->delete($record); $databaseManagementService->delete($record);
$this->fillForm(); $this->fillForm();
@ -754,7 +796,7 @@ class EditServer extends EditRecord
->columnSpan(4), ->columnSpan(4),
Actions::make([ Actions::make([
Action::make('createDatabase') Action::make('createDatabase')
->authorize(fn () => auth()->user()->can('create database')) ->authorize(fn () => auth()->user()->can('create', Database::class))
->disabled(fn () => DatabaseHost::query()->count() < 1) ->disabled(fn () => DatabaseHost::query()->count() < 1)
->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database')) ->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database'))
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary') ->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
@ -825,12 +867,12 @@ class EditServer extends EditRecord
Action::make('toggleInstall') Action::make('toggleInstall')
->label(trans('admin/server.toggle_install')) ->label(trans('admin/server.toggle_install'))
->disabled(fn (Server $server) => $server->isSuspended()) ->disabled(fn (Server $server) => $server->isSuspended())
->modal(fn (Server $server) => $server->status === ServerState::InstallFailed) ->modal(fn (Server $server) => $server->isFailedInstall())
->modalHeading(trans('admin/server.toggle_install_failed_header')) ->modalHeading(trans('admin/server.toggle_install_failed_header'))
->modalDescription(trans('admin/server.toggle_install_failed_desc')) ->modalDescription(trans('admin/server.toggle_install_failed_desc'))
->modalSubmitActionLabel(trans('admin/server.reinstall')) ->modalSubmitActionLabel(trans('admin/server.reinstall'))
->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) { ->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) {
if ($server->status === ServerState::InstallFailed) { if ($server->isFailedInstall()) {
try { try {
$reinstallService->handle($server); $reinstallService->handle($server);
@ -842,7 +884,7 @@ class EditServer extends EditRecord
} catch (Exception) { } catch (Exception) {
Notification::make() Notification::make()
->title(trans('admin/server.notifications.reinstall_failed')) ->title(trans('admin/server.notifications.reinstall_failed'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->danger() ->danger()
->send(); ->send();
} }
@ -889,7 +931,7 @@ class EditServer extends EditRecord
Notification::make() Notification::make()
->warning() ->warning()
->title(trans('admin/server.notifications.server_suspension')) ->title(trans('admin/server.notifications.server_suspension'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->send(); ->send();
} }
}), }),
@ -910,7 +952,7 @@ class EditServer extends EditRecord
Notification::make() Notification::make()
->warning() ->warning()
->title(trans('admin/server.notifications.server_suspension')) ->title(trans('admin/server.notifications.server_suspension'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->send(); ->send();
} }
}), }),
@ -973,7 +1015,7 @@ class EditServer extends EditRecord
} catch (Exception) { } catch (Exception) {
Notification::make() Notification::make()
->title(trans('admin/server.notifications.reinstall_failed')) ->title(trans('admin/server.notifications.reinstall_failed'))
->body(trans('admin/server.error_connecting', ['node' => $server->node->name])) ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->danger() ->danger()
->send(); ->send();
} }
@ -1021,7 +1063,8 @@ class EditServer extends EditRecord
]; ];
} }
protected function getHeaderActions(): array /** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = $this->getRecord(); $server = $this->getRecord();
@ -1091,7 +1134,7 @@ class EditServer extends EditRecord
$data['description'] = ''; $data['description'] = '';
} }
unset($data['docker'], $data['condition']); unset($data['docker'], $data['status'], $data['allocation_id']);
return $data; return $data;
} }

View File

@ -5,6 +5,8 @@ namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Server\Pages\Console; use App\Filament\Server\Pages\Console;
use App\Filament\Admin\Resources\ServerResource; use App\Filament\Admin\Resources\ServerResource;
use App\Models\Server; use App\Models\Server;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions; use Filament\Actions;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Actions\Action; use Filament\Actions\Action;
@ -17,6 +19,9 @@ use Filament\Tables\Table;
class ListServers extends ListRecords class ListServers extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ServerResource::class; protected static string $resource = ServerResource::class;
public function table(Table $table): Table public function table(Table $table): Table
@ -68,13 +73,13 @@ class ListServers extends ListRecords
->searchable(), ->searchable(),
SelectColumn::make('allocation_id') SelectColumn::make('allocation_id')
->label(trans('admin/server.primary_allocation')) ->label(trans('admin/server.primary_allocation'))
->hidden(!auth()->user()->can('update server')) ->hidden(!auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address])) ->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
->selectablePlaceholder(false) ->selectablePlaceholder(false)
->sortable(), ->sortable(),
TextColumn::make('allocation_id_readonly') TextColumn::make('allocation_id_readonly')
->label(trans('admin/server.primary_allocation')) ->label(trans('admin/server.primary_allocation'))
->hidden(auth()->user()->can('update server')) ->hidden(auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
->state(fn (Server $server) => $server->allocation->address), ->state(fn (Server $server) => $server->allocation->address),
TextColumn::make('image')->hidden(), TextColumn::make('image')->hidden(),
TextColumn::make('backups_count') TextColumn::make('backups_count')
@ -101,7 +106,8 @@ class ListServers extends ListRecords
]); ]);
} }
protected function getHeaderActions(): array /** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() Actions\CreateAction::make()

View File

@ -34,15 +34,18 @@ class AllocationsRelationManager extends RelationManager
{ {
return $table return $table
->selectCurrentPageOnly() ->selectCurrentPageOnly()
->recordTitleAttribute('ip') ->recordTitleAttribute('address')
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port") ->recordTitle(fn (Allocation $allocation) => $allocation->address)
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id) ->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
->inverseRelationship('server') ->inverseRelationship('server')
->heading(trans('admin/server.allocations')) ->heading(trans('admin/server.allocations'))
->columns([ ->columns([
TextColumn::make('ip')->label(trans('admin/server.ip_address')), TextColumn::make('ip')
TextColumn::make('port')->label(trans('admin/server.port')), ->label(trans('admin/server.ip_address')),
TextInputColumn::make('ip_alias')->label(trans('admin/server.alias')), TextColumn::make('port')
->label(trans('admin/server.port')),
TextInputColumn::make('ip_alias')
->label(trans('admin/server.alias')),
IconColumn::make('primary') IconColumn::make('primary')
->icon(fn ($state) => match ($state) { ->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled', true => 'tabler-star-filled',
@ -58,8 +61,11 @@ class AllocationsRelationManager extends RelationManager
]) ])
->actions([ ->actions([
Action::make('make-primary') Action::make('make-primary')
->label(trans('admin/server.make_primary'))
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : trans('admin/server.make_primary')), ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
DissociateAction::make()
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
]) ])
->headerActions([ ->headerActions([
CreateAction::make()->label(trans('admin/server.create_allocation')) CreateAction::make()->label(trans('admin/server.create_allocation'))
@ -69,7 +75,7 @@ class AllocationsRelationManager extends RelationManager
->options(collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip])) ->options(collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label(trans('admin/server.ip_address')) ->label(trans('admin/server.ip_address'))
->inlineLabel() ->inlineLabel()
->ipv4() ->ip()
->live() ->live()
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', [])) ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->required(), ->required(),
@ -85,9 +91,7 @@ class AllocationsRelationManager extends RelationManager
->inlineLabel() ->inlineLabel()
->live() ->live()
->disabled(fn (Get $get) => empty($get('allocation_ip'))) ->disabled(fn (Get $get) => empty($get('allocation_ip')))
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))))
CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))
)
->splitKeys(['Tab', ' ', ',']) ->splitKeys(['Tab', ' ', ','])
->required(), ->required(),
]) ])

View File

@ -6,6 +6,10 @@ use App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource\RelationManagers; use App\Filament\Admin\Resources\UserResource\RelationManagers;
use App\Models\Role; use App\Models\Role;
use App\Models\User; use App\Models\User;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
@ -22,6 +26,11 @@ use Filament\Schemas\Schema;
class UserResource extends Resource class UserResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = User::class; protected static ?string $model = User::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-users'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-users';
@ -53,7 +62,7 @@ class UserResource extends Resource
return static::getModel()::count() ?: null; return static::getModel()::count() ?: null;
} }
public static function table(Table $table): Table public static function defaultTable(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -101,9 +110,9 @@ class UserResource extends Resource
]); ]);
} }
public static function form(Schema $schema): Schema public static function form(Schema $form): Schema
{ {
return $schema return $form
->columns(['default' => 1, 'lg' => 3]) ->columns(['default' => 1, 'lg' => 3])
->components([ ->components([
TextInput::make('username') TextInput::make('username')
@ -148,14 +157,16 @@ class UserResource extends Resource
]); ]);
} }
public static function getRelations(): array /** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{ {
return [ return [
RelationManagers\ServersRelationManager::class, RelationManagers\ServersRelationManager::class,
]; ];
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListUsers::route('/'), 'index' => Pages\ListUsers::route('/'),

View File

@ -5,11 +5,18 @@ namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource; use App\Filament\Admin\Resources\UserResource;
use App\Models\Role; use App\Models\Role;
use App\Services\Users\UserCreationService; use App\Services\Users\UserCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class CreateUser extends CreateRecord class CreateUser extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class; protected static string $resource = UserResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
@ -21,7 +28,8 @@ class CreateUser extends CreateRecord
$this->service = $service; $this->service = $service;
} }
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -5,12 +5,19 @@ namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource; use App\Filament\Admin\Resources\UserResource;
use App\Models\User; use App\Models\User;
use App\Services\Users\UserUpdateService; use App\Services\Users\UserUpdateService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class EditUser extends EditRecord class EditUser extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class; protected static string $resource = UserResource::class;
private UserUpdateService $service; private UserUpdateService $service;
@ -20,7 +27,8 @@ class EditUser extends EditRecord
$this->service = $service; $this->service = $service;
} }
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()

View File

@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\UserResource\Pages; namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource; use App\Filament\Admin\Resources\UserResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords class ListUsers extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class; protected static string $resource = UserResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make(), CreateAction::make(),

View File

@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\UserResource\Pages; namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource; use App\Filament\Admin\Resources\UserResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord; use Filament\Resources\Pages\ViewRecord;
class ViewUser extends ViewRecord class ViewUser extends ViewRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class; protected static string $resource = UserResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),

View File

@ -6,9 +6,14 @@ use App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Models\WebhookConfiguration; use App\Models\WebhookConfiguration;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Form; use Filament\Schemas\Components\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
@ -19,6 +24,11 @@ use Filament\Schemas\Schema;
class WebhookResource extends Resource class WebhookResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = WebhookConfiguration::class; protected static ?string $model = WebhookConfiguration::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-webhook'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-webhook';
@ -50,7 +60,7 @@ class WebhookResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
public static function table(Table $table): Table public static function defaultTable(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -76,9 +86,9 @@ class WebhookResource extends Resource
]); ]);
} }
public static function form(Schema $schema): Schema public static function form(Schema $form): Schema
{ {
return $schema return $form
->schema([ ->schema([
TextInput::make('endpoint') TextInput::make('endpoint')
->label(trans('admin/webhook.endpoint')) ->label(trans('admin/webhook.endpoint'))
@ -99,7 +109,8 @@ class WebhookResource extends Resource
]); ]);
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListWebhookConfigurations::route('/'), 'index' => Pages\ListWebhookConfigurations::route('/'),

View File

@ -3,15 +3,23 @@
namespace App\Filament\Admin\Resources\WebhookResource\Pages; namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource; use App\Filament\Admin\Resources\WebhookResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
class CreateWebhookConfiguration extends CreateRecord class CreateWebhookConfiguration extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = WebhookResource::class; protected static string $resource = WebhookResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\WebhookResource\Pages; namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource; use App\Filament\Admin\Resources\WebhookResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditWebhookConfiguration extends EditRecord class EditWebhookConfiguration extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = WebhookResource::class; protected static string $resource = WebhookResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make(), DeleteAction::make(),

View File

@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource; use App\Filament\Admin\Resources\WebhookResource;
use App\Models\WebhookConfiguration; use App\Models\WebhookConfiguration;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
class ListWebhookConfigurations extends ListRecords class ListWebhookConfigurations extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = WebhookResource::class; protected static string $resource = WebhookResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\WebhookResource\Pages; namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource; use App\Filament\Admin\Resources\WebhookResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord; use Filament\Resources\Pages\ViewRecord;
class ViewWebhookConfiguration extends ViewRecord class ViewWebhookConfiguration extends ViewRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = WebhookResource::class; protected static string $resource = WebhookResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),

View File

@ -3,14 +3,13 @@
namespace App\Filament\Admin\Widgets; namespace App\Filament\Admin\Widgets;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Widgets\Widget; use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class CanaryWidget extends Widget class CanaryWidget extends FormWidget
{ {
protected string $view = 'filament.admin.widgets.canary-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 1; protected static ?int $sort = 1;
public static function canView(): bool public static function canView(): bool
@ -18,14 +17,28 @@ class CanaryWidget extends Widget
return config('app.version') === 'canary'; return config('app.version') === 'canary';
} }
public function getViewData(): array public function form(Form $form): Form
{ {
return [ return $form
'action' => Action::make('github') ->schema([
->label(trans('admin/dashboard.sections.intro-developers.button_issues')) Section::make(trans('admin/dashboard.sections.intro-developers.heading'))
->icon('tabler-brand-github') ->icon('tabler-code')
->url('https://github.com/pelican-dev/panel/issues', true) ->iconColor('primary')
->toHtmlString(), ->collapsible()
]; ->collapsed()
->persistCollapsed()
->schema([
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-developers.content')),
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-developers.extra_note')),
])
->headerActions([
Action::make('issues')
->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/issues', true),
]),
]);
} }
} }

View File

@ -0,0 +1,16 @@
<?php
namespace App\Filament\Admin\Widgets;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Widgets\Widget;
abstract class FormWidget extends Widget implements HasForms
{
use InteractsWithForms;
protected static bool $isLazy = false;
protected static string $view = 'filament.admin.widgets.form-widget';
}

View File

@ -2,25 +2,34 @@
namespace App\Filament\Admin\Widgets; namespace App\Filament\Admin\Widgets;
use Filament\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Widgets\Widget; use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class HelpWidget extends Widget class HelpWidget extends FormWidget
{ {
protected string $view = 'filament.admin.widgets.help-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 4; protected static ?int $sort = 4;
public function getViewData(): array public function form(Form $form): Form
{ {
return [ return $form
'action' => Action::make('docs') ->schema([
->label(trans('admin/dashboard.sections.intro-help.button_docs')) Section::make(trans('admin/dashboard.sections.intro-help.heading'))
->icon('tabler-speedboat') ->icon('tabler-question-mark')
->url('https://pelican.dev/docs', true) ->iconColor('info')
->toHtmlString(), ->collapsible()
]; ->persistCollapsed()
->schema([
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-help.content')),
])
->headerActions([
Action::make('docs')
->label(trans('admin/dashboard.sections.intro-help.button_docs'))
->icon('tabler-speedboat')
->url('https://pelican.dev/docs', true),
]),
]);
} }
} }

View File

@ -4,15 +4,13 @@ namespace App\Filament\Admin\Widgets;
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode; use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
use App\Models\Node; use App\Models\Node;
use Filament\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Widgets\Widget; use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class NoNodesWidget extends Widget class NoNodesWidget extends FormWidget
{ {
protected string $view = 'filament.admin.widgets.no-nodes-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 2; protected static ?int $sort = 2;
public static function canView(): bool public static function canView(): bool
@ -20,14 +18,25 @@ class NoNodesWidget extends Widget
return Node::count() <= 0; return Node::count() <= 0;
} }
public function getViewData(): array public function form(Form $form): Form
{ {
return [ return $form
'action' => Action::make('create-node') ->schema([
->label(trans('admin/dashboard.sections.intro-first-node.button_label')) Section::make(trans('admin/dashboard.sections.intro-first-node.heading'))
->icon('tabler-server-2') ->icon('tabler-server-2')
->url(CreateNode::getUrl()) ->iconColor('primary')
->toHtmlString(), ->collapsible()
]; ->persistCollapsed()
->schema([
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-first-node.content')),
])
->headerActions([
Action::make('create-node')
->label(trans('admin/dashboard.sections.intro-first-node.button_label'))
->icon('tabler-server-2')
->url(CreateNode::getUrl()),
]),
]);
} }
} }

View File

@ -2,26 +2,37 @@
namespace App\Filament\Admin\Widgets; namespace App\Filament\Admin\Widgets;
use Filament\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Widgets\Widget; use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class SupportWidget extends Widget class SupportWidget extends FormWidget
{ {
protected string $view = 'filament.admin.widgets.support-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 3; protected static ?int $sort = 3;
public function getViewData(): array public function form(Form $form): Form
{ {
return [ return $form
'action' => Action::make('donate') ->schema([
->label(trans('admin/dashboard.sections.intro-support.button_donate')) Section::make(trans('admin/dashboard.sections.intro-support.heading'))
->icon('tabler-cash') ->icon('tabler-heart-filled')
->url('https://pelican.dev/donate', true) ->iconColor('danger')
->color('success') ->collapsible()
->toHtmlString(), ->persistCollapsed()
]; ->schema([
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-support.content')),
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-support.extra_note')),
])
->headerActions([
Action::make('donate')
->label(trans('admin/dashboard.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url('https://pelican.dev/donate', true)
->color('success'),
]),
]);
} }
} }

View File

@ -3,15 +3,13 @@
namespace App\Filament\Admin\Widgets; namespace App\Filament\Admin\Widgets;
use App\Services\Helpers\SoftwareVersionService; use App\Services\Helpers\SoftwareVersionService;
use Filament\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Widgets\Widget; use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class UpdateWidget extends Widget class UpdateWidget extends FormWidget
{ {
protected string $view = 'filament.admin.widgets.update-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 0; protected static ?int $sort = 0;
private SoftwareVersionService $softwareVersionService; private SoftwareVersionService $softwareVersionService;
@ -21,18 +19,34 @@ class UpdateWidget extends Widget
$this->softwareVersionService = $softwareVersionService; $this->softwareVersionService = $softwareVersionService;
} }
public function getViewData(): array public function form(Form $form): Form
{ {
return [ $isLatest = $this->softwareVersionService->isLatestPanel();
'version' => $this->softwareVersionService->currentPanelVersion(),
'latestVersion' => $this->softwareVersionService->latestPanelVersion(), return $form
'isLatest' => $this->softwareVersionService->isLatestPanel(), ->schema([
'action' => Action::make('update') $isLatest
->label(trans('admin/dashboard.sections.intro-update-available.heading')) ? Section::make(trans('admin/dashboard.sections.intro-no-update.heading'))
->icon('tabler-clipboard-text') ->icon('tabler-checkbox')
->url('https://pelican.dev/docs/panel/update', true) ->iconColor('success')
->color('warning') ->schema([
->toHtmlString(), Placeholder::make('')
]; ->content(trans('admin/dashboard.sections.intro-no-update.content', ['version' => $this->softwareVersionService->currentPanelVersion()])),
])
: Section::make(trans('admin/dashboard.sections.intro-update-available.heading'))
->icon('tabler-info-circle')
->iconColor('warning')
->schema([
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-update-available.content', ['latestVersion' => $this->softwareVersionService->latestPanelVersion()])),
])
->headerActions([
Action::make('update')
->label(trans('admin/dashboard.sections.intro-update-available.heading'))
->icon('tabler-clipboard-text')
->url('https://pelican.dev/docs/panel/update', true)
->color('warning'),
]),
]);
} }
} }

View File

@ -6,7 +6,13 @@ use App\Enums\ServerResourceType;
use App\Filament\App\Resources\ServerResource; use App\Filament\App\Resources\ServerResource;
use App\Filament\Components\Tables\Columns\ServerEntryColumn; use App\Filament\Components\Tables\Columns\ServerEntryColumn;
use App\Filament\Server\Pages\Console; use App\Filament\Server\Pages\Console;
use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Notifications\Notification;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Schemas\Components\Tabs\Tab; use Filament\Schemas\Components\Tabs\Tab;
use Filament\Support\Enums\TextSize; use Filament\Support\Enums\TextSize;
@ -16,48 +22,64 @@ use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException;
use Livewire\Attributes\On;
class ListServers extends ListRecords class ListServers extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ServerResource::class; protected static string $resource = ServerResource::class;
public const DANGER_THRESHOLD = 0.9; public const DANGER_THRESHOLD = 0.9;
public const WARNING_THRESHOLD = 0.7; public const WARNING_THRESHOLD = 0.7;
public function table(Table $table): Table private DaemonPowerRepository $daemonPowerRepository;
{
$baseQuery = auth()->user()->accessibleServers();
$viewOne = [ public function boot(): void
{
$this->daemonPowerRepository = new DaemonPowerRepository();
}
/** @return Stack[] */
protected function gridColumns(): array
{
return [
Stack::make([
ServerEntryColumn::make('server_entry')
->searchable(['name']),
]),
];
}
/** @return Column[] */
protected function tableColumns(): array
{
return [
TextColumn::make('condition') TextColumn::make('condition')
->label('') ->label('')
->default('unknown') ->default('unknown')
->wrap() ->wrap()
->size(TextSize::Medium) ->size(TextSize::Medium)
->badge() ->badge()
->alignCenter()
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time)) ->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
->icon(fn (Server $server) => $server->condition->getIcon()) ->icon(fn (Server $server) => $server->condition->getIcon())
->color(fn (Server $server) => $server->condition->getColor()), ->color(fn (Server $server) => $server->condition->getColor()),
];
$viewTwo = [
TextColumn::make('name') TextColumn::make('name')
->description(fn (Server $server) => $server->description)
->grow()
->label('') ->label('')
->size(TextSize::Medium) ->size(TextSize::Medium)
->searchable(), ->searchable(),
TextColumn::make('iNeedAName') TextColumn::make('allocation.address')
->label('') ->label('')
->badge() ->badge()
->size(TextSize::Medium) ->visibleFrom('md')
->copyable(request()->isSecure()) ->copyable(request()->isSecure()),
->copyMessage(fn (Server $server, string $state) => 'Copied ' . $server->allocation->address)
->state(fn (Server $server) => $server->allocation->address),
];
$viewThree = [
TextColumn::make('cpuUsage') TextColumn::make('cpuUsage')
->label('Resources')
->label('') ->label('')
->size(TextSize::Medium) ->size(TextSize::Medium)
->icon('tabler-cpu') ->icon('tabler-cpu')
@ -66,6 +88,7 @@ class ListServers extends ListRecords
->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')), ->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
TextColumn::make('memoryUsage') TextColumn::make('memoryUsage')
->label('') ->label('')
->icon('tabler-device-desktop-analytics')
->size(TextSize::Medium) ->size(TextSize::Medium)
->icon('tabler-memory') ->icon('tabler-memory')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true)) ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true))
@ -73,42 +96,30 @@ class ListServers extends ListRecords
->color(fn (Server $server) => $this->getResourceColor($server, 'memory')), ->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
TextColumn::make('diskUsage') TextColumn::make('diskUsage')
->label('') ->label('')
->icon('tabler-device-sd-card')
->size(TextSize::Medium) ->size(TextSize::Medium)
->icon('tabler-device-floppy') ->icon('tabler-device-floppy')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true)) ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true))
->state(fn (Server $server) => $server->formatResource('disk_bytes')) ->state(fn (Server $server) => $server->formatResource('disk_bytes'))
->color(fn (Server $server) => $this->getResourceColor($server, 'disk')), ->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
]; ];
}
public function table(Table $table): Table
{
$baseQuery = auth()->user()->accessibleServers();
$usingGrid = (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid';
return $table return $table
->paginated(false) ->paginated(false)
->query(fn () => $baseQuery) ->query(fn () => $baseQuery)
->poll('15s') ->poll('15s')
->columns( ->columns($usingGrid ? $this->gridColumns() : $this->tableColumns())
(auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid' ->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null)
? [ ->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions()) : [])
Stack::make([ ->actionsAlignment(Alignment::Center->value)
ServerEntryColumn::make('server_entry') ->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null)
->searchable(['name']),
]),
]
: [
ColumnGroup::make('Status')
->label('Status')
->columns($viewOne),
ColumnGroup::make('Server')
->label('Servers')
->columns($viewTwo),
ColumnGroup::make('Resources')
->label('Resources')
->columns($viewThree),
]
)
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
->contentGrid([
'default' => 1,
'md' => 2,
])
->emptyStateIcon('tabler-brand-docker') ->emptyStateIcon('tabler-brand-docker')
->emptyStateDescription('') ->emptyStateDescription('')
->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!') ->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!')
@ -151,36 +162,33 @@ class ListServers extends ListRecords
]; ];
} }
public function getResourceColor(Server $server, string $resource): ?string protected function getResourceColor(Server $server, string $resource): ?string
{ {
$current = null; $current = null;
$limit = null; $limit = null;
switch ($resource) { switch ($resource) {
case 'cpu': case 'cpu':
$current = $server->resources()['cpu_absolute'] ?? 0; $current = $server->retrieveResources()['cpu_absolute'] ?? 0;
$limit = $server->cpu; $limit = $server->cpu;
if ($server->cpu === 0) { if ($server->cpu === 0) {
return null; return null;
} }
break; break;
case 'memory': case 'memory':
$current = $server->resources()['memory_bytes'] ?? 0; $current = $server->retrieveResources()['memory_bytes'] ?? 0;
$limit = $server->memory * 2 ** 20; $limit = $server->memory * 2 ** 20;
if ($server->memory === 0) { if ($server->memory === 0) {
return null; return null;
} }
break; break;
case 'disk': case 'disk':
$current = $server->resources()['disk_bytes'] ?? 0; $current = $server->retrieveResources()['disk_bytes'] ?? 0;
$limit = $server->disk * 2 ** 20; $limit = $server->disk * 2 ** 20;
if ($server->disk === 0) { if ($server->disk === 0) {
return null; return null;
} }
break; break;
default: default:
return null; return null;
} }
@ -194,6 +202,60 @@ class ListServers extends ListRecords
} }
return null; return null;
}
#[On('powerAction')]
public function powerAction(Server $server, string $action): void
{
try {
$this->daemonPowerRepository->setServer($server)->send($action);
Notification::make()
->title('Power Action')
->body($action . ' sent to ' . $server->name)
->success()
->send();
cache()->forget("servers.$server->uuid.status");
$this->redirect(self::getUrl(['activeTab' => $this->activeTab]));
} catch (ConnectionException) {
Notification::make()
->title(trans('exceptions.node.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
}
/** @return Action[] */
public static function getPowerActions(): array
{
return [
Action::make('start')
->color('primary')
->icon('tabler-player-play-filled')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']),
Action::make('restart')
->color('gray')
->icon('tabler-reload')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isRestartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']),
Action::make('stop')
->color('danger')
->icon('tabler-player-stop-filled')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStoppable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']),
Action::make('kill')
->color('danger')
->icon('tabler-alert-square')
->tooltip('This can result in data corruption and/or data loss!')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isKillable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']),
];
} }
} }

View File

@ -25,7 +25,7 @@ class RotateDatabasePasswordAction extends Action
$this->icon('tabler-refresh'); $this->icon('tabler-refresh');
$this->authorize(fn (Database $database) => auth()->user()->can('update database', $database)); $this->authorize(fn (Database $database) => auth()->user()->can('update', $database));
$this->modalHeading(trans('admin/databasehost.rotate_password')); $this->modalHeading(trans('admin/databasehost.rotate_password'));

View File

@ -6,5 +6,5 @@ use Filament\Tables\Columns\Column;
class ServerEntryColumn extends Column class ServerEntryColumn extends Column
{ {
protected string $view = 'tables.columns.server-entry-column'; protected string $view = 'livewire.columns.server-entry-column';
} }

View File

@ -12,6 +12,8 @@ use App\Services\Helpers\LanguageService;
use App\Services\Users\ToggleTwoFactorService; use App\Services\Users\ToggleTwoFactorService;
use App\Services\Users\TwoFactorSetupService; use App\Services\Users\TwoFactorSetupService;
use App\Services\Users\UserUpdateService; use App\Services\Users\UserUpdateService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use chillerlan\QRCode\Common\EccLevel; use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Common\Version; use chillerlan\QRCode\Common\Version;
use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QRCode;
@ -41,6 +43,7 @@ use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rules\Password; use Illuminate\Validation\Rules\Password;
use Laravel\Socialite\Facades\Socialite; use Laravel\Socialite\Facades\Socialite;
@ -50,6 +53,9 @@ use Laravel\Socialite\Facades\Socialite;
*/ */
class EditProfile extends \Filament\Auth\Pages\EditProfile class EditProfile extends \Filament\Auth\Pages\EditProfile
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
private ToggleTwoFactorService $toggleTwoFactorService; private ToggleTwoFactorService $toggleTwoFactorService;
public function boot(ToggleTwoFactorService $toggleTwoFactorService): void public function boot(ToggleTwoFactorService $toggleTwoFactorService): void
@ -288,10 +294,12 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
$get('allowed_ips'), $get('allowed_ips'),
); );
Activity::event('user:api-key.create') Activity::event('user:api-key.create')
->subject($token->accessToken) ->actor($user)
->property('identifier', $token->accessToken->identifier) ->subject($user)
->log(); ->subject($token->accessToken)
->property('identifier', $token->accessToken->identifier)
->log();
Notification::make() Notification::make()
->title(trans('profile.key_created')) ->title(trans('profile.key_created'))
@ -363,6 +371,95 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
'grid' => trans('profile.grid'), 'grid' => trans('profile.grid'),
'table' => trans('profile.table'), 'table' => trans('profile.table'),
]), ]),
Section::make(trans('profile.console'))
->collapsible()
->icon('tabler-brand-tabler')
->columns(4)
->schema([
TextInput::make('console_font_size')
->label(trans('profile.font_size'))
->columnSpan(1)
->minValue(1)
->numeric()
->required()
->default(14),
Select::make('console_font')
->label(trans('profile.font'))
->required()
->options(function () {
$fonts = [
'monospace' => 'monospace', //default
];
if (!Storage::disk('public')->exists('fonts')) {
Storage::disk('public')->makeDirectory('fonts');
$this->fillForm();
}
foreach (Storage::disk('public')->allFiles('fonts') as $file) {
$fileInfo = pathinfo($file);
if ($fileInfo['extension'] === 'ttf') {
$fonts[$fileInfo['filename']] = $fileInfo['filename'];
}
}
return $fonts;
})
->reactive()
->default('monospace')
->afterStateUpdated(fn ($state, Set $set) => $set('font_preview', $state)),
Placeholder::make('font_preview')
->label(trans('profile.font_preview'))
->columnSpan(2)
->content(function (Get $get) {
$fontName = $get('console_font') ?? 'monospace';
$fontSize = $get('console_font_size') . 'px';
$style = <<<CSS
.preview-text {
font-family: $fontName;
font-size: $fontSize;
margin-top: 10px;
display: block;
}
CSS;
if ($fontName !== 'monospace') {
$fontUrl = asset("storage/fonts/$fontName.ttf");
$style = <<<CSS
@font-face {
font-family: $fontName;
src: url("$fontUrl");
}
$style
CSS;
}
return new HtmlString(<<<HTML
<style>
{$style}
</style>
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
HTML);
}),
TextInput::make('console_graph_period')
->label(trans('profile.graph_period'))
->suffix(trans('profile.seconds'))
->hintIcon('tabler-question-mark')
->hintIconTooltip(trans('profile.graph_period_helper'))
->columnSpan(2)
->numeric()
->default(30)
->minValue(10)
->maxValue(120)
->required(),
TextInput::make('console_rows')
->label(trans('profile.rows'))
->minValue(1)
->numeric()
->required()
->columnSpan(2)
->default(30),
]),
]), ]),
Section::make(trans('profile.console')) Section::make(trans('profile.console'))
->collapsible() ->collapsible()
@ -435,7 +532,8 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
return []; return [];
} }
protected function getHeaderActions(): array /** @return array<HeaderAction|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getSaveFormAction()->formId('form'), $this->getSaveFormAction()->formId('form'),
@ -446,12 +544,14 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
protected function mutateFormDataBeforeSave(array $data): array protected function mutateFormDataBeforeSave(array $data): array
{ {
$moarbetterdata = [ $moarbetterdata = [
'console_font' => $data['console_font'],
'console_font_size' => $data['console_font_size'], 'console_font_size' => $data['console_font_size'],
'console_rows' => $data['console_rows'], 'console_rows' => $data['console_rows'],
'console_graph_period' => $data['console_graph_period'],
'dashboard_layout' => $data['dashboard_layout'], 'dashboard_layout' => $data['dashboard_layout'],
]; ];
unset($data['dashboard_layout'], $data['console_font_size'], $data['console_rows']); unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout']);
$data['customization'] = json_encode($moarbetterdata); $data['customization'] = json_encode($moarbetterdata);
return $data; return $data;
@ -461,8 +561,10 @@ class EditProfile extends \Filament\Auth\Pages\EditProfile
{ {
$moarbetterdata = json_decode($data['customization'], true); $moarbetterdata = json_decode($data['customization'], true);
$data['console_font'] = $moarbetterdata['console_font'] ?? 'monospace';
$data['console_font_size'] = $moarbetterdata['console_font_size'] ?? 14; $data['console_font_size'] = $moarbetterdata['console_font_size'] ?? 14;
$data['console_rows'] = $moarbetterdata['console_rows'] ?? 30; $data['console_rows'] = $moarbetterdata['console_rows'] ?? 30;
$data['console_graph_period'] = $moarbetterdata['console_graph_period'] ?? 30;
$data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid'; $data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid';
return $data; return $data;

View File

@ -2,8 +2,10 @@
namespace App\Filament\Pages\Auth; namespace App\Filament\Pages\Auth;
use App\Events\Auth\ProvidedAuthenticationToken;
use App\Extensions\Captcha\Providers\CaptchaProvider; use App\Extensions\Captcha\Providers\CaptchaProvider;
use App\Extensions\OAuth\Providers\OAuthProvider; use App\Extensions\OAuth\Providers\OAuthProvider;
use App\Facades\Activity;
use App\Models\User; use App\Models\User;
use Filament\Auth\Http\Responses\LoginResponse; use Filament\Auth\Http\Responses\LoginResponse;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -54,14 +56,37 @@ class Login extends \Filament\Auth\Pages\Login
if ($token === null) { if ($token === null) {
$this->verifyTwoFactor = true; $this->verifyTwoFactor = true;
Activity::event('auth:checkpoint')
->withRequestMetadata()
->subject($user)
->log();
return null; return null;
} }
$isValidToken = $this->google2FA->verifyKey( $isValidToken = false;
$user->totp_secret, if (strlen($token) === $this->google2FA->getOneTimePasswordLength()) {
$token, $isValidToken = $this->google2FA->verifyKey(
Config::integer('panel.auth.2fa.window'), $user->totp_secret,
); $token,
Config::integer('panel.auth.2fa.window'),
);
if ($isValidToken) {
event(new ProvidedAuthenticationToken($user));
}
} else {
foreach ($user->recoveryTokens as $recoveryToken) {
if (password_verify($token, $recoveryToken->token)) {
$isValidToken = true;
$recoveryToken->delete();
event(new ProvidedAuthenticationToken($user, true));
break;
}
}
}
if (!$isValidToken) { if (!$isValidToken) {
// Buffer to prevent bruteforce // Buffer to prevent bruteforce
@ -103,7 +128,9 @@ class Login extends \Filament\Auth\Pages\Login
{ {
return TextInput::make('2fa') return TextInput::make('2fa')
->label(trans('auth.two-factor-code')) ->label(trans('auth.two-factor-code'))
->hidden(fn () => !$this->verifyTwoFactor) ->hintIcon('tabler-question-mark')
->hintIconTooltip(trans('auth.two-factor-hint'))
->visible(fn () => $this->verifyTwoFactor)
->required() ->required()
->live(); ->live();
} }

View File

@ -2,59 +2,29 @@
namespace App\Filament\Server\Components; namespace App\Filament\Server\Components;
use BackedEnum;
use Closure; use Closure;
use Filament\Schemas\Components\Component; use Filament\Support\Concerns\EvaluatesClosures;
use Filament\Schemas\Components\Concerns\CanOpenUrl; use Filament\Widgets\StatsOverviewWidget\Stat;
use Filament\Schemas\Components\Concerns\HasDescription; use Illuminate\Contracts\View\View;
use Illuminate\Contracts\Support\Htmlable;
class SmallStatBlock extends Component class SmallStatBlock extends Component
{ {
use CanOpenUrl; use EvaluatesClosures;
use HasDescription;
protected string $view = 'filament.components.server-small-data-block'; protected bool|Closure $copyOnClick = false;
protected string|BackedEnum|null $icon = null; public function copyOnClick(bool|Closure $copyOnClick = true): static
{
$this->copyOnClick = $copyOnClick;
protected string $value; protected string $value;
final public function __construct(string $label, string $value) public function shouldCopyOnClick(): bool
{ {
$this->label($label); return $this->evaluate($this->copyOnClick);
$this->value($value);
} }
/** public function render(): View
* @return SmallStatBlock
*/
public static function make(string $label, string $value): static
{
return app(static::class, ['label' => $label, 'value' => $value]);
}
public function icon(string|BackedEnum|null $icon): static
{
$this->icon = $icon;
return $this;
}
/**
* @return SmallStatBlock
*/
private function value(string $value): static
{
$this->value = $value;
return $this;
}
/**
* @return scalar | Htmlable | Closure
*/
public function getValue(): mixed
{ {
return value($this->value); return value($this->value);
} }

View File

@ -1,44 +0,0 @@
<?php
namespace App\Filament\Server\Components;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Contracts\Support\Htmlable;
class StatBlock extends Stat
{
protected string|\Closure|Htmlable|null $label;
protected string $view = 'filament.components.server-data-block';
protected $value;
public function label(string $label): static
{
$this->label = $label;
return $this;
}
public function value($value): static
{
$this->value = $value;
return $this;
}
public function getLabel(): string
{
return $this->label;
}
public function getValue(): mixed
{
return value($this->value);
}
public function toHtml(): string
{
return $this->render()->render();
}
}

View File

@ -9,13 +9,16 @@ use App\Extensions\Features\FeatureProvider;
use App\Filament\Server\Widgets\ServerConsole; use App\Filament\Server\Widgets\ServerConsole;
use App\Filament\Server\Widgets\ServerCpuChart; use App\Filament\Server\Widgets\ServerCpuChart;
use App\Filament\Server\Widgets\ServerMemoryChart; use App\Filament\Server\Widgets\ServerMemoryChart;
// use App\Filament\Server\Widgets\ServerNetworkChart; use App\Filament\Server\Widgets\ServerNetworkChart;
use App\Filament\Server\Widgets\ServerOverview; use App\Filament\Server\Widgets\ServerOverview;
use App\Livewire\AlertBanner; use App\Livewire\AlertBanner;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Traits\Filament\CanCustomizeHeaderActions;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Support\Enums\Size; use Filament\Support\Enums\Size;
use Filament\Widgets\Widget; use Filament\Widgets\Widget;
@ -24,7 +27,8 @@ use Livewire\Attributes\On;
class Console extends Page class Console extends Page
{ {
protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-tabler'; use CanCustomizeHeaderActions;
use InteractsWithActions;
protected static ?int $navigationSort = 1; protected static ?int $navigationSort = 1;
@ -109,7 +113,7 @@ class Console extends Page
$allWidgets = array_merge($allWidgets, [ $allWidgets = array_merge($allWidgets, [
ServerCpuChart::class, ServerCpuChart::class,
ServerMemoryChart::class, ServerMemoryChart::class,
//ServerNetworkChart::class, TODO: convert units. ServerNetworkChart::class,
]); ]);
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Bottom->value] ?? []); $allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Bottom->value] ?? []);
@ -142,7 +146,8 @@ class Console extends Page
$this->cacheHeaderActions(); $this->cacheHeaderActions();
} }
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();

View File

@ -3,6 +3,9 @@
namespace App\Filament\Server\Pages; namespace App\Filament\Server\Pages;
use App\Models\Server; use App\Models\Server;
use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Schemas\Components\Form; use Filament\Schemas\Components\Form;
@ -15,6 +18,9 @@ use Filament\Schemas\Schema;
*/ */
abstract class ServerFormPage extends Page abstract class ServerFormPage extends Page
{ {
use BlockAccessInConflict;
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use InteractsWithFormActions; use InteractsWithFormActions;
use InteractsWithForms; use InteractsWithForms;
@ -52,17 +58,4 @@ abstract class ServerFormPage extends Page
return $server; return $server;
} }
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
} }

View File

@ -2,19 +2,40 @@
namespace App\Filament\Server\Resources; namespace App\Filament\Server\Resources;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\ActivityResource\Pages; use App\Filament\Server\Resources\ActivityResource\Pages;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Role; use App\Models\Role;
use App\Models\Server; use App\Models\Server;
use App\Models\User; use App\Models\User;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyTable;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\HtmlString;
class ActivityResource extends Resource class ActivityResource extends Resource
{ {
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyTable;
protected static ?string $model = ActivityLog::class; protected static ?string $model = ActivityLog::class;
protected static ?string $modelLabel = 'Activity'; protected static ?string $modelLabel = 'Activity';
@ -27,12 +48,96 @@ class ActivityResource extends Resource
protected static bool $isScopedToTenant = false; protected static bool $isScopedToTenant = false;
public static function defaultTable(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->paginated([25, 50])
->defaultPaginationPageOption(25)
->columns([
TextColumn::make('event')
->html()
->description(fn ($state) => $state)
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
TextColumn::make('user')
->state(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
$user .= " ({$activityLog->actor->email})";
}
return $user;
})
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor) ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
->grow(false),
DateTimeColumn::make('timestamp')
->since()
->sortable()
->grow(false),
])
->defaultSort('timestamp', 'desc')
->actions([
ViewAction::make()
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
->form([
Placeholder::make('event')
->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
TextInput::make('user')
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
$user .= " ({$activityLog->actor->email})";
}
if (auth()->user()->can('seeIps activityLog')) {
$user .= " - $activityLog->ip";
}
return $user;
})
->hintAction(
Action::make('edit')
->label(trans('filament-actions::edit.single.label'))
->icon('tabler-edit')
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor))
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
),
DateTimePicker::make('timestamp'),
KeyValue::make('properties')
->label('Metadata')
->formatStateUsing(fn ($state) => Arr::dot($state)),
]),
])
->filters([
SelectFilter::make('event')
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
->searchable()
->preload(),
]);
}
public static function getEloquentQuery(): Builder public static function getEloquentQuery(): Builder
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id)) return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id)->where('subject_type', $server->getMorphClass()))
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS) ->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) { ->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
// We could do this with a query and a lot of joins, but that gets pretty // We could do this with a query and a lot of joins, but that gets pretty
@ -58,7 +163,8 @@ class ActivityResource extends Resource
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListActivities::route('/'), 'index' => Pages\ListActivities::route('/'),

View File

@ -2,114 +2,18 @@
namespace App\Filament\Server\Resources\ActivityResource\Pages; namespace App\Filament\Server\Resources\ActivityResource\Pages;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
use App\Filament\Server\Resources\ActivityResource; use App\Filament\Server\Resources\ActivityResource;
use App\Models\ActivityLog; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Models\Server;
use App\Models\User;
use Filament\Facades\Filament;
use Filament\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\KeyValue;
use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Arr;
use Illuminate\Support\HtmlString;
class ListActivities extends ListRecords class ListActivities extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ActivityResource::class; protected static string $resource = ActivityResource::class;
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->paginated([25, 50])
->defaultPaginationPageOption(25)
->columns([
TextColumn::make('event')
->html()
->description(fn ($state) => $state)
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
TextColumn::make('user')
->state(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
$user .= " ({$activityLog->actor->email})";
}
return $user;
})
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user') ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
->grow(false),
DateTimeColumn::make('timestamp')
->since()
->sortable()
->grow(false),
])
->defaultSort('timestamp', 'desc')
->actions([
ViewAction::make()
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
->schema([
TextEntry::make('event')
->state(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
TextInput::make('user')
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
$user .= " ({$activityLog->actor->email})";
}
if (auth()->user()->can('seeIps activityLog')) {
$user .= " - $activityLog->ip";
}
return $user;
})
->hintAction(
Action::make('edit')
->label(trans('filament-actions::edit.single.label'))
->icon('tabler-edit')
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user'))
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
),
DateTimePicker::make('timestamp'),
KeyValue::make('properties')
->label('Metadata')
->formatStateUsing(fn ($state) => Arr::dot($state)),
]),
])
->filters([
SelectFilter::make('event')
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
->searchable()
->preload(),
]);
}
public function getBreadcrumbs(): array public function getBreadcrumbs(): array
{ {
return []; return [];

View File

@ -2,16 +2,32 @@
namespace App\Filament\Server\Resources; namespace App\Filament\Server\Resources;
use App\Facades\Activity;
use App\Filament\Server\Resources\AllocationResource\Pages; use App\Filament\Server\Resources\AllocationResource\Pages;
use App\Models\Allocation; use App\Models\Allocation;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyTable;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\DetachAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class AllocationResource extends Resource class AllocationResource extends Resource
{ {
use BlockAccessInConflict;
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyTable;
protected static ?string $model = Allocation::class; protected static ?string $model = Allocation::class;
protected static ?string $modelLabel = 'Network'; protected static ?string $modelLabel = 'Network';
@ -22,17 +38,59 @@ class AllocationResource extends Resource
protected static string|\BackedEnum|null $navigationIcon = 'tabler-network'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-network';
// TODO: find better way handle server conflict state public static function defaultTable(Table $table): Table
public static function canAccess(): bool
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
if ($server->isInConflictState()) { return $table
return false; ->columns([
} TextColumn::make('ip')
->label('Address')
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
TextColumn::make('alias')
->hidden(),
TextColumn::make('port'),
TextInputColumn::make('notes')
->visibleFrom('sm')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
->label('Notes')
->placeholder('No Notes'),
IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled',
default => 'tabler-star',
})
->color(fn ($state) => match ($state) {
true => 'warning',
default => 'gray',
})
->action(function (Allocation $allocation) use ($server) {
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
return $server->update(['allocation_id' => $allocation->id]);
}
})
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label('Primary'),
])
->actions([
DetachAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label('Delete')
->icon('tabler-trash')
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
'notes' => null,
'server_id' => null,
]);
return parent::canAccess(); Activity::event('server:allocation.delete')
->subject($allocation)
->property('allocation', $allocation->address)
->log();
}),
]);
} }
public static function canViewAny(): bool public static function canViewAny(): bool
@ -55,7 +113,8 @@ class AllocationResource extends Resource
return auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant());
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListAllocations::route('/'), 'index' => Pages\ListAllocations::route('/'),

View File

@ -4,85 +4,31 @@ namespace App\Filament\Server\Resources\AllocationResource\Pages;
use App\Facades\Activity; use App\Facades\Activity;
use App\Filament\Server\Resources\AllocationResource; use App\Filament\Server\Resources\AllocationResource;
use App\Models\Allocation;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Services\Allocations\FindAssignableAllocationService; use App\Services\Allocations\FindAssignableAllocationService;
use Filament\Actions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Actions\DetachAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
class ListAllocations extends ListRecords class ListAllocations extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = AllocationResource::class; protected static string $resource = AllocationResource::class;
public function table(Table $table): Table /** @return array<Action|ActionGroup> */
{ protected function getDefaultHeaderActions(): array
/** @var Server $server */
$server = Filament::getTenant();
return $table
->columns([
TextColumn::make('ip')
->label('Address')
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
TextColumn::make('alias')
->hidden(),
TextColumn::make('port'),
TextInputColumn::make('notes')
->visibleFrom('sm')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
->label('Notes')
->placeholder('No Notes'),
IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled',
default => 'tabler-star',
})
->color(fn ($state) => match ($state) {
true => 'warning',
default => 'gray',
})
->action(function (Allocation $allocation) use ($server) {
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
return $server->update(['allocation_id' => $allocation->id]);
}
})
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label('Primary'),
])
->actions([
DetachAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label('Delete')
->icon('tabler-trash')
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
'notes' => null,
'server_id' => null,
]);
Activity::event('server:allocation.delete')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
}),
]);
}
protected function getHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return [ return [
Actions\Action::make('addAllocation') Action::make('addAllocation')
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server)) ->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation') ->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
->hidden(fn () => !config('panel.client_features.allocations.enabled')) ->hidden(fn () => !config('panel.client_features.allocations.enabled'))
@ -93,7 +39,7 @@ class ListAllocations extends ListRecords
Activity::event('server:allocation.create') Activity::event('server:allocation.create')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->toString()) ->property('allocation', $allocation->address)
->log(); ->log();
}), }),
]; ];

View File

@ -2,16 +2,54 @@
namespace App\Filament\Server\Resources; namespace App\Filament\Server\Resources;
use App\Enums\BackupStatus;
use App\Enums\ServerState;
use App\Facades\Activity;
use App\Filament\Server\Resources\BackupResource\Pages; use App\Filament\Server\Resources\BackupResource\Pages;
use App\Http\Controllers\Api\Client\Servers\BackupController;
use App\Models\Backup; use App\Models\Backup;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Services\Backups\DownloadLinkService;
use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Services\Backups\DeleteBackupService;
use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use App\Traits\Filament\HasLimitBadge;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Request;
class BackupResource extends Resource class BackupResource extends Resource
{ {
use BlockAccessInConflict;
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
use HasLimitBadge;
protected static ?string $model = Backup::class; protected static ?string $model = Backup::class;
protected static ?int $navigationSort = 3; protected static ?int $navigationSort = 3;
@ -20,45 +58,151 @@ class BackupResource extends Resource
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
public const WARNING_THRESHOLD = 0.7; protected static function getBadgeCount(): int
public static function getNavigationBadge(): string
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$limit = $server->backup_limit; return $server->backups->count();
return $server->backups->count() . ($limit === 0 ? '' : ' / ' . $limit);
} }
public static function getNavigationBadgeColor(): ?string protected static function getBadgeLimit(): int
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$limit = $server->backup_limit; return $server->backup_limit;
$count = $server->backups->count();
if ($limit === 0) {
return null;
}
return $count >= $limit ? 'danger'
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
} }
// TODO: find better way handle server conflict state public static function defaultForm(Form $form): Form
public static function canAccess(): bool {
return $form
->schema([
TextInput::make('name')
->label('Name')
->columnSpanFull(),
TextArea::make('ignored')
->columnSpanFull()
->label('Ignored Files & Directories'),
Toggle::make('is_locked')
->label('Lock?')
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
]);
}
public static function defaultTable(Table $table): Table
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
if ($server->isInConflictState()) { return $table
return false; ->columns([
} TextColumn::make('name')
->searchable(),
BytesColumn::make('bytes')
->label('Size'),
DateTimeColumn::make('created_at')
->label('Created')
->since()
->sortable(),
TextColumn::make('status')
->label('Status')
->badge(),
IconColumn::make('is_locked')
->visibleFrom('md')
->label('Lock Status')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->actions([
ActionGroup::make([
Action::make('lock')
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('download')
->color('primary')
->icon('tabler-download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('restore')
->color('success')
->icon('tabler-folder-up')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
->form([
Placeholder::make('')
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
Checkbox::make('truncate')
->label('Delete all files before restoring backup?'),
])
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
if (!is_null($server->status)) {
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This server is not currently in a state that allows for a backup to be restored.')
->send();
}
return parent::canAccess(); if (!$backup->is_successful && is_null($backup->completed_at)) {
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This backup cannot be restored at this time: not completed or failed.')
->send();
}
$log = Activity::event('server:backup.restore')
->subject($backup)
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
// If the backup is for an S3 file we need to generate a unique Download link for
// it that will allow daemon to actually access the file.
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
$url = $downloadLinkService->handle($backup, auth()->user());
}
// Update the status right away for the server so that we know not to allow certain
// actions against it via the Panel API.
$server->update(['status' => ServerState::RestoringBackup]);
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
});
return Notification::make()
->title('Restoring Backup')
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
DeleteAction::make('delete')
->disabled(fn (Backup $backup) => $backup->is_locked)
->modalDescription(fn (Backup $backup) => 'Do you wish to delete ' . $backup->name . '?')
->modalSubmitActionLabel('Delete Backup')
->action(function (Backup $backup, DeleteBackupService $deleteBackupService) {
try {
$deleteBackupService->handle($backup);
} catch (ConnectionException) {
Notification::make()
->title('Could not delete backup')
->body('Connection to node failed')
->danger()
->send();
return;
}
Activity::event('server:backup.delete')
->subject($backup)
->property(['name' => $backup->name, 'failed' => !$backup->is_successful])
->log();
})
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
]),
]);
} }
public static function canViewAny(): bool public static function canViewAny(): bool
@ -76,7 +220,8 @@ class BackupResource extends Resource
return auth()->user()->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant());
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListBackups::route('/'), 'index' => Pages\ListBackups::route('/'),

View File

@ -2,166 +2,37 @@
namespace App\Filament\Server\Resources\BackupResource\Pages; namespace App\Filament\Server\Resources\BackupResource\Pages;
use App\Enums\BackupStatus;
use App\Enums\ServerState;
use App\Facades\Activity; use App\Facades\Activity;
use App\Filament\Server\Resources\BackupResource; use App\Filament\Server\Resources\BackupResource;
use App\Http\Controllers\Api\Client\Servers\BackupController;
use App\Models\Backup;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Services\Backups\DownloadLinkService;
use App\Services\Backups\InitiateBackupService; use App\Services\Backups\InitiateBackupService;
use App\Filament\Components\Tables\Columns\BytesColumn; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions;
use Filament\Facades\Filament;
use Filament\Forms\Components\Checkbox;
use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\CreateAction;
use Filament\Tables\Columns\IconColumn; use Filament\Facades\Filament;
use Filament\Tables\Columns\TextColumn; use Filament\Notifications\Notification;
use Filament\Tables\Table; use Filament\Resources\Pages\ListRecords;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
class ListBackups extends ListRecords class ListBackups extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = BackupResource::class; protected static string $resource = BackupResource::class;
protected static bool $canCreateAnother = false; /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
public function form(Schema $schema): Schema
{
return $schema
->schema([
TextInput::make('name')
->label('Name')
->columnSpanFull(),
TextArea::make('ignored')
->columnSpanFull()
->label('Ignored Files & Directories'),
Toggle::make('is_locked')
->label('Lock?')
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
]);
}
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->columns([
TextColumn::make('name')
->searchable(),
BytesColumn::make('bytes')
->label('Size'),
DateTimeColumn::make('created_at')
->label('Created')
->since()
->sortable(),
TextColumn::make('status')
->label('Status')
->badge(),
IconColumn::make('is_locked')
->visibleFrom('md')
->label('Lock Status')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->actions([
ActionGroup::make([
Action::make('lock')
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('download')
->color('primary')
->icon('tabler-download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('restore')
->color('success')
->icon('tabler-folder-up')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
->schema([
TextEntry::make('INeedAName')
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
Checkbox::make('truncate')
->label('Delete all files before restoring backup?'),
])
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
if (!is_null($server->status)) {
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This server is not currently in a state that allows for a backup to be restored.')
->send();
}
if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This backup cannot be restored at this time: not completed or failed.')
->send();
}
$log = Activity::event('server:backup.restore')
->subject($backup)
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
// If the backup is for an S3 file we need to generate a unique Download link for
// it that will allow daemon to actually access the file.
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
$url = $downloadLinkService->handle($backup, auth()->user());
}
// Update the status right away for the server so that we know not to allow certain
// actions against it via the Panel API.
$server->update(['status' => ServerState::RestoringBackup]);
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
});
return Notification::make()
->title('Restoring Backup')
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
DeleteAction::make('delete')
->disabled(fn (Backup $backup) => $backup->is_locked)
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
->modalSubmitActionLabel('Delete Backup')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
]),
]);
}
protected function getHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return [ return [
Actions\CreateAction::make() CreateAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server)) ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup') ->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
->disabled(fn () => $server->backups()->count() >= $server->backup_limit) ->disabled(fn () => $server->backups()->count() >= $server->backup_limit)

View File

@ -2,62 +2,118 @@
namespace App\Filament\Server\Resources; namespace App\Filament\Server\Resources;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\DatabaseResource\Pages; use App\Filament\Server\Resources\DatabaseResource\Pages;
use App\Models\Database; use App\Models\Database;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Services\Databases\DatabaseManagementService;
use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use App\Traits\Filament\HasLimitBadge;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class DatabaseResource extends Resource class DatabaseResource extends Resource
{ {
use BlockAccessInConflict;
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
use HasLimitBadge;
protected static ?string $model = Database::class; protected static ?string $model = Database::class;
protected static ?int $navigationSort = 6; protected static ?int $navigationSort = 6;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-database'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-database';
public const WARNING_THRESHOLD = 0.7; protected static function getBadgeCount(): int
public static function getNavigationBadge(): string
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$limit = $server->database_limit; return $server->databases->count();
return $server->databases->count() . ($limit === 0 ? '' : ' / ' . $limit);
} }
public static function getNavigationBadgeColor(): ?string protected static function getBadgeLimit(): int
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$limit = $server->database_limit; return $server->database_limit;
$count = $server->databases->count();
if ($limit === 0) {
return null;
}
return $count >= $limit
? 'danger'
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
} }
// TODO: find better way handle server conflict state public static function defaultForm(Form $form): Form
public static function canAccess(): bool
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
if ($server->isInConflictState()) { return $form
return false; ->schema([
} TextInput::make('host')
->formatStateUsing(fn (Database $database) => $database->address())
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('database')
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('username')
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('password')
->password()->revealable()
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->hintAction(
RotateDatabasePasswordAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
)
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->label('Connections From'),
TextInput::make('max_connections')
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
TextInput::make('jdbc')
->label('JDBC Connection String')
->password()->revealable()
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->columnSpanFull()
->formatStateUsing(fn (Database $database) => $database->jdbc),
]);
}
return parent::canAccess(); public static function defaultTable(Table $table): Table
{
return $table
->columns([
TextColumn::make('host')
->state(fn (Database $database) => $database->address())
->badge(),
TextColumn::make('database'),
TextColumn::make('username'),
TextColumn::make('remote'),
DateTimeColumn::make('created_at')
->sortable(),
])
->actions([
ViewAction::make()
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
DeleteAction::make()
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
]);
} }
public static function canViewAny(): bool public static function canViewAny(): bool
@ -85,7 +141,8 @@ class DatabaseResource extends Resource
return auth()->user()->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant());
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListDatabases::route('/'), 'index' => Pages\ListDatabases::route('/'),

View File

@ -2,90 +2,30 @@
namespace App\Filament\Server\Resources\DatabaseResource\Pages; namespace App\Filament\Server\Resources\DatabaseResource\Pages;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\DatabaseResource; use App\Filament\Server\Resources\DatabaseResource;
use App\Models\Database;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Services\Databases\DatabaseManagementService; use App\Services\Databases\DatabaseManagementService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Schemas\Components\Grid; use Filament\Schemas\Components\Grid;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Form;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Schemas\Schema;
class ListDatabases extends ListRecords class ListDatabases extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseResource::class; protected static string $resource = DatabaseResource::class;
public function form(Schema $schema): Schema /** @return array<Action|ActionGroup> */
{ protected function getDefaultHeaderActions(): array
/** @var Server $server */
$server = Filament::getTenant();
return $schema
->schema([
TextInput::make('host')
->formatStateUsing(fn (Database $database) => $database->address()),
//TODO ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('database'),
//TODO ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('username'),
//TODO->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('password')
->password()->revealable()
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->hintAction(
RotateDatabasePasswordAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
)
//TODO ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->label('Connections From'),
TextInput::make('max_connections')
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
TextInput::make('jdbc')
->label('JDBC Connection String')
->password()->revealable()
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
//TODO ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->columnSpanFull()
->formatStateUsing(fn (Database $database) => $database->jdbc),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('host')
->state(fn (Database $database) => $database->address())
->badge(),
TextColumn::make('database'),
TextColumn::make('username'),
TextColumn::make('remote'),
DateTimeColumn::make('created_at')
->sortable(),
])
->actions([
ViewAction::make()
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
DeleteAction::make()
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
]);
}
protected function getHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();

View File

@ -5,13 +5,20 @@ namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\FileResource\Pages; use App\Filament\Server\Resources\FileResource\Pages;
use App\Models\File; use App\Models\File;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class FileResource extends Resource class FileResource extends Resource
{ {
use BlockAccessInConflict;
use CanCustomizePages;
use CanCustomizeRelations;
protected static ?string $model = File::class; protected static ?string $model = File::class;
protected static ?int $navigationSort = 2; protected static ?int $navigationSort = 2;
@ -20,19 +27,6 @@ class FileResource extends Resource
protected static bool $isScopedToTenant = false; protected static bool $isScopedToTenant = false;
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
public static function canViewAny(): bool public static function canViewAny(): bool
{ {
return auth()->user()->can(Permission::ACTION_FILE_READ, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_FILE_READ, Filament::getTenant());
@ -53,7 +47,8 @@ class FileResource extends Resource
return auth()->user()->can(Permission::ACTION_FILE_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_FILE_DELETE, Filament::getTenant());
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'edit' => Pages\EditFiles::route('/edit/{path}'), 'edit' => Pages\EditFiles::route('/edit/{path}'),

View File

@ -11,6 +11,8 @@ use App\Livewire\AlertBanner;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository; use App\Repositories\Daemon\DaemonFileRepository;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\CodeEditor; use Filament\Forms\Components\CodeEditor;
@ -26,6 +28,7 @@ use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment; use Filament\Support\Enums\Alignment;
use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Routing\Route; use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade; use Illuminate\Support\Facades\Route as RouteFacade;
@ -36,6 +39,8 @@ use Livewire\Attributes\Locked;
*/ */
class EditFiles extends Page class EditFiles extends Page
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use InteractsWithFormActions; use InteractsWithFormActions;
use InteractsWithForms; use InteractsWithForms;
@ -178,6 +183,15 @@ class EditFiles extends Page
->info() ->info()
->closable() ->closable()
->send(); ->send();
try {
$this->getDaemonFileRepository()->getDirectory('/');
} catch (ConnectionException) {
AlertBanner::make('node_connection_error')
->title('Could not connect to the node!')
->danger()
->send();
}
} }
} }
@ -232,6 +246,14 @@ class EditFiles extends Page
return $this->fileRepository; return $this->fileRepository;
} }
/**
* @param array<string, mixed> $parameters
*/
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
{
return parent::getUrl($parameters, $isAbsolute, $panel, $tenant) . '/';
}
public static function route(string $path): PageRegistration public static function route(string $path): PageRegistration
{ {
return new PageRegistration( return new PageRegistration(

View File

@ -40,20 +40,16 @@ use Livewire\Attributes\Locked;
class ListFiles extends ListRecords class ListFiles extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = FileResource::class; protected static string $resource = FileResource::class;
#[Locked] #[Locked]
public string $path; public string $path = '/';
private DaemonFileRepository $fileRepository; private DaemonFileRepository $fileRepository;
public function mount(?string $path = null): void
{
parent::mount();
$this->path = $path ?? '/';
}
public function getBreadcrumbs(): array public function getBreadcrumbs(): array
{ {
$resource = static::getResource(); $resource = static::getResource();
@ -321,9 +317,9 @@ class ListFiles extends ListRecords
->label('') ->label('')
->icon('tabler-trash') ->icon('tabler-trash')
->requiresConfirmation() ->requiresConfirmation()
->modalDescription(fn (File $file) => $file->name) ->modalHeading(fn (File $file) => trans('filament-actions::delete.single.modal.heading', ['label' => $file->name . ' ' . ($file->is_directory ? 'folder' : 'file')]))
->modalHeading('Delete file?')
->action(function (File $file) { ->action(function (File $file) {
$this->deselectAllTableRecords();
$this->getDaemonFileRepository()->deleteFiles($this->path, [$file->name]); $this->getDaemonFileRepository()->deleteFiles($this->path, [$file->name]);
Activity::event('server:file.delete') Activity::event('server:file.delete')
@ -406,10 +402,8 @@ class ListFiles extends ListRecords
]); ]);
} }
/** /** @return array<HeaderAction|HeaderActionGroup> */
* @throws \Exception protected function getDefaultHeaderActions(): array
*/
protected function getHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
@ -507,8 +501,9 @@ class ListFiles extends ListRecords
->schema([ ->schema([
TextInput::make('searchTerm') TextInput::make('searchTerm')
->placeholder('Enter a search term, e.g. *.txt') ->placeholder('Enter a search term, e.g. *.txt')
->required()
->regex('/^[^*]*\*?[^*]*$/') ->regex('/^[^*]*\*?[^*]*$/')
->minLength(3), ->minValue(3),
]) ])
->action(fn ($data) => redirect(SearchFiles::getUrl([ ->action(fn ($data) => redirect(SearchFiles::getUrl([
'searchTerm' => $data['searchTerm'], 'searchTerm' => $data['searchTerm'],

View File

@ -7,14 +7,20 @@ use App\Models\File;
use App\Models\Server; use App\Models\Server;
use App\Filament\Components\Tables\Columns\BytesColumn; use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
class SearchFiles extends ListRecords class SearchFiles extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = FileResource::class; protected static string $resource = FileResource::class;
protected static ?string $title = 'Global Search'; protected static ?string $title = 'Global Search';
@ -22,15 +28,8 @@ class SearchFiles extends ListRecords
#[Locked] #[Locked]
public string $searchTerm; public string $searchTerm;
#[Locked] #[Url]
public string $path; public string $path = '/';
public function mount(?string $searchTerm = null, ?string $path = null): void
{
parent::mount();
$this->searchTerm = $searchTerm;
$this->path = $path ?? '/';
}
public function getBreadcrumbs(): array public function getBreadcrumbs(): array
{ {

View File

@ -2,12 +2,18 @@
namespace App\Filament\Server\Resources; namespace App\Filament\Server\Resources;
use App\Facades\Activity;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\ScheduleResource\Pages; use App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager; use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
use App\Helpers\Utilities; use App\Helpers\Utilities;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Schedule; use App\Models\Schedule;
use App\Models\Server; use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Carbon\Carbon; use Carbon\Carbon;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
@ -21,33 +27,34 @@ use Filament\Schemas\Components\Form;
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Support\Enums\Operation; use Filament\Support\Enums\Operation;
use Filament\Support\Exceptions\Halt; use Filament\Support\Exceptions\Halt;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
class ScheduleResource extends Resource class ScheduleResource extends Resource
{ {
use BlockAccessInConflict;
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = Schedule::class; protected static ?string $model = Schedule::class;
protected static ?int $navigationSort = 4; protected static ?int $navigationSort = 4;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-clock'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-clock';
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
public static function canViewAny(): bool public static function canViewAny(): bool
{ {
return auth()->user()->can(Permission::ACTION_SCHEDULE_READ, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_SCHEDULE_READ, Filament::getTenant());
@ -310,14 +317,54 @@ class ScheduleResource extends Resource
]); ]);
} }
public static function getRelations(): array public static function defaultTable(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('cron')
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
TextColumn::make('status')
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
IconColumn::make('only_when_online')
->boolean()
->sortable(),
DateTimeColumn::make('last_run_at')
->label('Last run')
->placeholder('Never')
->since()
->sortable(),
DateTimeColumn::make('next_run_at')
->label('Next run')
->placeholder('Never')
->since()
->sortable()
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
])
->actions([
ViewAction::make(),
EditAction::make(),
DeleteAction::make()
->after(function (Schedule $schedule) {
Activity::event('server:schedule.delete')
->subject($schedule)
->property('name', $schedule->name)
->log();
}),
]);
}
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{ {
return [ return [
TasksRelationManager::class, TasksRelationManager::class,
]; ];
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListSchedules::route('/'), 'index' => Pages\ListSchedules::route('/'),

View File

@ -6,11 +6,16 @@ use App\Facades\Activity;
use App\Filament\Server\Resources\ScheduleResource; use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Schedule; use App\Models\Schedule;
use App\Models\Server; use App\Models\Server;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
class CreateSchedule extends CreateRecord class CreateSchedule extends CreateRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ScheduleResource::class; protected static string $resource = ScheduleResource::class;
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;

View File

@ -6,10 +6,15 @@ use App\Facades\Activity;
use App\Filament\Server\Resources\ScheduleResource; use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Schedule; use App\Models\Schedule;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeHeaderActions;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
class EditSchedule extends EditRecord class EditSchedule extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ScheduleResource::class; protected static string $resource = ScheduleResource::class;
protected function afterSave(): void protected function afterSave(): void
@ -35,7 +40,8 @@ class EditSchedule extends EditRecord
return $data; return $data;
} }
protected function getHeaderActions(): array /** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()

View File

@ -2,65 +2,27 @@
namespace App\Filament\Server\Resources\ScheduleResource\Pages; namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Facades\Activity;
use App\Filament\Server\Resources\ScheduleResource; use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Schedule; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions; use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ListSchedules extends ListRecords class ListSchedules extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ScheduleResource::class; protected static string $resource = ScheduleResource::class;
public function table(Table $table): Table /** @return array<Action|ActionGroup> */
{ protected function getDefaultHeaderActions(): array
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('cron')
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
TextColumn::make('status')
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
IconColumn::make('only_when_online')
->boolean()
->sortable(),
DateTimeColumn::make('last_run_at')
->label('Last run')
->placeholder('Never')
->since()
->sortable(),
DateTimeColumn::make('next_run_at')
->label('Next run')
->placeholder('Never')
->since()
->sortable()
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
])
->actions([
ViewAction::make(),
EditAction::make(),
DeleteAction::make()
->after(function (Schedule $schedule) {
Activity::event('server:schedule.delete')
->subject($schedule)
->property('name', $schedule->name)
->log();
}),
]);
}
protected function getHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make()->label('New Schedule'), CreateAction::make()
->label('New Schedule'),
]; ];
} }

View File

@ -7,6 +7,8 @@ use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Schedule; use App\Models\Schedule;
use App\Services\Schedules\ProcessScheduleService; use App\Services\Schedules\ProcessScheduleService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -14,9 +16,13 @@ use Filament\Resources\Pages\ViewRecord;
class ViewSchedule extends ViewRecord class ViewSchedule extends ViewRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ScheduleResource::class; protected static string $resource = ScheduleResource::class;
protected function getHeaderActions(): array /** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Action::make('runNow') Action::make('runNow')

View File

@ -8,6 +8,11 @@ use App\Models\Server;
use App\Models\User; use App\Models\User;
use App\Services\Subusers\SubuserDeletionService; use App\Services\Subusers\SubuserDeletionService;
use App\Services\Subusers\SubuserUpdateService; use App\Services\Subusers\SubuserUpdateService;
use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyTable;
use App\Traits\Filament\HasLimitBadge;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
@ -20,6 +25,7 @@ use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Tables\Columns\ImageColumn; use Filament\Tables\Columns\ImageColumn;
@ -29,6 +35,12 @@ use Illuminate\Database\Eloquent\Model;
class UserResource extends Resource class UserResource extends Resource
{ {
use BlockAccessInConflict;
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyTable;
use HasLimitBadge;
protected static ?string $model = User::class; protected static ?string $model = User::class;
protected static ?int $navigationSort = 5; protected static ?int $navigationSort = 5;
@ -37,25 +49,12 @@ class UserResource extends Resource
protected static ?string $tenantOwnershipRelationshipName = 'subServers'; protected static ?string $tenantOwnershipRelationshipName = 'subServers';
public static function getNavigationBadge(): string protected static function getBadgeCount(): int
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return (string) $server->subusers->count(); return $server->subusers->count();
}
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
} }
public static function canViewAny(): bool public static function canViewAny(): bool
@ -78,11 +77,40 @@ class UserResource extends Resource
return auth()->user()->can(Permission::ACTION_USER_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_USER_DELETE, Filament::getTenant());
} }
public static function table(Table $table): Table public static function defaultTable(Table $table): Table
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$tabs = [];
$permissionsArray = [];
foreach (Permission::permissionData() as $data) {
$options = [];
$descriptions = [];
foreach ($data['permissions'] as $permission) {
$options[$permission] = str($permission)->headline();
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
->icon($data['icon'])
->schema([
CheckboxList::make($data['name'])
->label('')
->bulkToggleable()
->columns(2)
->options($options)
->descriptions($descriptions),
]),
]);
}
return $table return $table
->paginated(false) ->paginated(false)
->searchable(false) ->searchable(false)
@ -158,69 +186,8 @@ class UserResource extends Resource
Actions::make([ Actions::make([
Action::make('assignAll') Action::make('assignAll')
->label('Assign All') ->label('Assign All')
->action(function (Set $set) { ->action(function (Set $set) use ($permissionsArray) {
$permissions = [ $permissions = $permissionsArray;
'control' => [
'console',
'start',
'stop',
'restart',
],
'user' => [
'read',
'create',
'update',
'delete',
],
'file' => [
'read',
'read-content',
'create',
'update',
'delete',
'archive',
'sftp',
],
'backup' => [
'read',
'create',
'delete',
'download',
'restore',
],
'allocation' => [
'read',
'create',
'update',
'delete',
],
'startup' => [
'read',
'update',
'docker-image',
],
'database' => [
'read',
'create',
'update',
'delete',
'view_password',
],
'schedule' => [
'read',
'create',
'update',
'delete',
],
'settings' => [
'rename',
'reinstall',
],
'activity' => [
'read',
],
];
foreach ($permissions as $key => $value) { foreach ($permissions as $key => $value) {
$allValues = array_unique($value); $allValues = array_unique($value);
$set($key, $allValues); $set($key, $allValues);
@ -235,268 +202,30 @@ class UserResource extends Resource
]), ]),
Tabs::make() Tabs::make()
->columnSpanFull() ->columnSpanFull()
->schema([ ->schema($tabs),
Tab::make('Console')
->schema([
Section::make()
->description(trans('server/users.permissions.control_desc'))
->icon('tabler-terminal-2')
->schema([
CheckboxList::make('control')
->formatStateUsing(function (User $user, Set $set) use ($server) {
$permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions;
$transformedPermissions = [];
foreach ($permissionsArray as $permission) {
[$group, $action] = explode('.', $permission, 2);
$transformedPermissions[$group][] = $action;
}
foreach ($transformedPermissions as $key => $value) {
$set($key, $value);
}
return $transformedPermissions['control'] ?? [];
})
->bulkToggleable()
->label('')
->columns(2)
->options([
'console' => 'Console',
'start' => 'Start',
'stop' => 'Stop',
'restart' => 'Restart',
])
->descriptions([
'console' => trans('server/users.permissions.control_console'),
'start' => trans('server/users.permissions.control_start'),
'stop' => trans('server/users.permissions.control_stop'),
'restart' => trans('server/users.permissions.control_restart'),
]),
]),
]),
Tab::make('User')
->schema([
Section::make()
->description(trans('server/users.permissions.user_desc'))
->icon('tabler-users')
->schema([
CheckboxList::make('user')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'create' => trans('server/users.permissions.user_create'),
'read' => trans('server/users.permissions.user_read'),
'update' => trans('server/users.permissions.user_update'),
'delete' => trans('server/users.permissions.user_delete'),
]),
]),
]),
Tab::make('File')
->schema([
Section::make()
->description(trans('server/users.permissions.file_desc'))
->icon('tabler-folders')
->schema([
CheckboxList::make('file')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'read-content' => 'Read Content',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
'archive' => 'Archive',
'sftp' => 'SFTP',
])
->descriptions([
'create' => trans('server/users.permissions.file_create'),
'read' => trans('server/users.permissions.file_read'),
'read-content' => trans('server/users.permissions.file_read_content'),
'update' => trans('server/users.permissions.file_update'),
'delete' => trans('server/users.permissions.file_delete'),
'archive' => trans('server/users.permissions.file_archive'),
'sftp' => trans('server/users.permissions.file_sftp'),
]),
]),
]),
Tab::make('Backup')
->schema([
Section::make()
->description(trans('server/users.permissions.backup_desc'))
->icon('tabler-download')
->schema([
CheckboxList::make('backup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'delete' => 'Delete',
'download' => 'Download',
'restore' => 'Restore',
])
->descriptions([
'create' => trans('server/users.permissions.backup_create'),
'read' => trans('server/users.permissions.backup_read'),
'delete' => trans('server/users.permissions.backup_delete'),
'download' => trans('server/users.permissions.backup_download'),
'restore' => trans('server/users.permissions.backup_restore'),
]),
]),
]),
Tab::make('Allocation')
->schema([
Section::make()
->description(trans('server/users.permissions.allocation_desc'))
->icon('tabler-network')
->schema([
CheckboxList::make('allocation')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'read' => trans('server/users.permissions.allocation_read'),
'create' => trans('server/users.permissions.allocation_create'),
'update' => trans('server/users.permissions.allocation_update'),
'delete' => trans('server/users.permissions.allocation_delete'),
]),
]),
]),
Tab::make('Startup')
->schema([
Section::make()
->description(trans('server/users.permissions.startup_desc'))
->icon('tabler-question-mark')
->schema([
CheckboxList::make('startup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'update' => 'Update',
'docker-image' => 'Docker Image',
])
->descriptions([
'read' => trans('server/users.permissions.startup_read'),
'update' => trans('server/users.permissions.startup_update'),
'docker-image' => trans('server/users.permissions.startup_docker_image'),
]),
]),
]),
Tab::make('Database')
->schema([
Section::make()
->description(trans('server/users.permissions.database_desc'))
->icon('tabler-database')
->schema([
CheckboxList::make('database')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
'view_password' => 'View Password',
])
->descriptions([
'read' => trans('server/users.permissions.database_read'),
'create' => trans('server/users.permissions.database_create'),
'update' => trans('server/users.permissions.database_update'),
'delete' => trans('server/users.permissions.database_delete'),
'view_password' => trans('server/users.permissions.database_view_password'),
]),
]),
]),
Tab::make('Schedule')
->schema([
Section::make()
->description(trans('server/users.permissions.schedule_desc'))
->icon('tabler-clock')
->schema([
CheckboxList::make('schedule')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'read' => trans('server/users.permissions.schedule_read'),
'create' => trans('server/users.permissions.schedule_create'),
'update' => trans('server/users.permissions.schedule_update'),
'delete' => trans('server/users.permissions.schedule_delete'),
]),
]),
]),
Tab::make('Settings')
->schema([
Section::make()
->description(trans('server/users.permissions.settings_desc'))
->icon('tabler-settings')
->schema([
CheckboxList::make('settings')
->bulkToggleable()
->label('')
->columns(2)
->options([
'rename' => 'Rename',
'reinstall' => 'Reinstall',
])
->descriptions([
'rename' => trans('server/users.permissions.setting_rename'),
'reinstall' => trans('server/users.permissions.setting_reinstall'),
]),
]),
]),
Tab::make('Activity')
->schema([
Section::make()
->description(trans('server/users.permissions.activity_desc'))
->icon('tabler-stack')
->schema([
CheckboxList::make('activity')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
])
->descriptions([
'read' => trans('server/users.permissions.activity_read'),
]),
]),
]),
]),
]), ]),
]), ])
->mutateRecordDataUsing(function ($data, User $user) use ($server) {
$permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions;
$transformedPermissions = [];
foreach ($permissionsArray as $permission) {
[$group, $action] = explode('.', $permission, 2);
$transformedPermissions[$group][] = $action;
}
foreach ($transformedPermissions as $key => $value) {
$data[$key] = $value;
}
return $data;
}),
]); ]);
} }
public static function getPages(): array /** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListUsers::route('/'), 'index' => Pages\ListUsers::route('/'),

View File

@ -7,6 +7,8 @@ use App\Filament\Server\Resources\UserResource;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Services\Subusers\SubuserCreationService; use App\Services\Subusers\SubuserCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception; use Exception;
use Filament\Actions\CreateAction; use Filament\Actions\CreateAction;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -25,13 +27,46 @@ use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords class ListUsers extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class; protected static string $resource = UserResource::class;
protected function getHeaderActions(): array /** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$tabs = [];
$permissionsArray = [];
foreach (Permission::permissionData() as $data) {
$options = [];
$descriptions = [];
foreach ($data['permissions'] as $permission) {
$options[$permission] = str($permission)->headline();
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
->icon($data['icon'])
->schema([
CheckboxList::make($data['name'])
->label('')
->bulkToggleable()
->columns(2)
->options($options)
->descriptions($descriptions),
]),
]);
}
return [ return [
CreateAction::make('invite') CreateAction::make('invite')
->label('Invite User') ->label('Invite User')
@ -60,72 +95,10 @@ class ListUsers extends ListRecords
Actions::make([ Actions::make([
Action::make('assignAll') Action::make('assignAll')
->label('Assign All') ->label('Assign All')
->action(function (Set $set, Get $get) { ->action(function (Set $set, Get $get) use ($permissionsArray) {
$permissions = [ $permissions = $permissionsArray;
'control' => [
'console',
'start',
'stop',
'restart',
],
'user' => [
'read',
'create',
'update',
'delete',
],
'file' => [
'read',
'read-content',
'create',
'update',
'delete',
'archive',
'sftp',
],
'backup' => [
'read',
'create',
'delete',
'download',
'restore',
],
'allocation' => [
'read',
'create',
'update',
'delete',
],
'startup' => [
'read',
'update',
'docker-image',
],
'database' => [
'read',
'create',
'update',
'delete',
'view_password',
],
'schedule' => [
'read',
'create',
'update',
'delete',
],
'settings' => [
'rename',
'reinstall',
],
'activity' => [
'read',
],
];
foreach ($permissions as $key => $value) { foreach ($permissions as $key => $value) {
$currentValues = $get($key) ?? []; $allValues = array_unique($value);
$allValues = array_unique(array_merge($currentValues, $value));
$set($key, $allValues); $set($key, $allValues);
} }
}), }),
@ -138,247 +111,7 @@ class ListUsers extends ListRecords
]), ]),
Tabs::make() Tabs::make()
->columnSpanFull() ->columnSpanFull()
->schema([ ->schema($tabs),
Tab::make('Console')
->schema([
Section::make()
->description(trans('server/users.permissions.control_desc'))
->icon('tabler-terminal-2')
->schema([
CheckboxList::make('control')
->bulkToggleable()
->label('')
->columns(2)
->options([
'console' => 'Console',
'start' => 'Start',
'stop' => 'Stop',
'restart' => 'Restart',
])
->descriptions([
'console' => trans('server/users.permissions.control_console'),
'start' => trans('server/users.permissions.control_start'),
'stop' => trans('server/users.permissions.control_stop'),
'restart' => trans('server/users.permissions.control_restart'),
]),
]),
]),
Tab::make('User')
->schema([
Section::make()
->description(trans('server/users.permissions.user_desc'))
->icon('tabler-users')
->schema([
CheckboxList::make('user')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'create' => trans('server/users.permissions.user_create'),
'read' => trans('server/users.permissions.user_read'),
'update' => trans('server/users.permissions.user_update'),
'delete' => trans('server/users.permissions.user_delete'),
]),
]),
]),
Tab::make('File')
->schema([
Section::make()
->description(trans('server/users.permissions.file_desc'))
->icon('tabler-folders')
->schema([
CheckboxList::make('file')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'read-content' => 'Read Content',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
'archive' => 'Archive',
'sftp' => 'SFTP',
])
->descriptions([
'create' => trans('server/users.permissions.file_create'),
'read' => trans('server/users.permissions.file_read'),
'read-content' => trans('server/users.permissions.file_read_content'),
'update' => trans('server/users.permissions.file_update'),
'delete' => trans('server/users.permissions.file_delete'),
'archive' => trans('server/users.permissions.file_archive'),
'sftp' => trans('server/users.permissions.file_sftp'),
]),
]),
]),
Tab::make('Backup')
->schema([
Section::make()
->description(trans('server/users.permissions.backup_desc'))
->icon('tabler-download')
->schema([
CheckboxList::make('backup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'delete' => 'Delete',
'download' => 'Download',
'restore' => 'Restore',
])
->descriptions([
'create' => trans('server/users.permissions.backup_create'),
'read' => trans('server/users.permissions.backup_read'),
'delete' => trans('server/users.permissions.backup_delete'),
'download' => trans('server/users.permissions.backup_download'),
'restore' => trans('server/users.permissions.backup_restore'),
]),
]),
]),
Tab::make('Allocation')
->schema([
Section::make()
->description(trans('server/users.permissions.allocation_desc'))
->icon('tabler-network')
->schema([
CheckboxList::make('allocation')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'read' => trans('server/users.permissions.allocation_read'),
'create' => trans('server/users.permissions.allocation_create'),
'update' => trans('server/users.permissions.allocation_update'),
'delete' => trans('server/users.permissions.allocation_delete'),
]),
]),
]),
Tab::make('Startup')
->schema([
Section::make()
->description(trans('server/users.permissions.startup_desc'))
->icon('tabler-question-mark')
->schema([
CheckboxList::make('startup')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'update' => 'Update',
'docker-image' => 'Docker Image',
])
->descriptions([
'read' => trans('server/users.permissions.startup_read'),
'update' => trans('server/users.permissions.startup_update'),
'docker-image' => trans('server/users.permissions.startup_docker_image'),
]),
]),
]),
Tab::make('Database')
->schema([
Section::make()
->description(trans('server/users.permissions.database_desc'))
->icon('tabler-database')
->schema([
CheckboxList::make('database')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
'view_password' => 'View Password',
])
->descriptions([
'read' => trans('server/users.permissions.database_read'),
'create' => trans('server/users.permissions.database_create'),
'update' => trans('server/users.permissions.database_update'),
'delete' => trans('server/users.permissions.database_delete'),
'view_password' => trans('server/users.permissions.database_view_password'),
]),
]),
]),
Tab::make('Schedule')
->schema([
Section::make()
->description(trans('server/users.permissions.schedule_desc'))
->icon('tabler-clock')
->schema([
CheckboxList::make('schedule')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
])
->descriptions([
'read' => trans('server/users.permissions.schedule_read'),
'create' => trans('server/users.permissions.schedule_create'),
'update' => trans('server/users.permissions.schedule_update'),
'delete' => trans('server/users.permissions.schedule_delete'),
]),
]),
]),
Tab::make('Settings')
->schema([
Section::make()
->description(trans('server/users.permissions.settings_desc'))
->icon('tabler-settings')
->schema([
CheckboxList::make('settings')
->bulkToggleable()
->label('')
->columns(2)
->options([
'rename' => 'Rename',
'reinstall' => 'Reinstall',
])
->descriptions([
'rename' => trans('server/users.permissions.setting_rename'),
'reinstall' => trans('server/users.permissions.setting_reinstall'),
]),
]),
]),
Tab::make('Activity')
->schema([
Section::make()
->description(trans('server/users.permissions.activity_desc'))
->icon('tabler-stack')
->schema([
CheckboxList::make('activity')
->bulkToggleable()
->label('')
->columns(2)
->options([
'read' => 'Read',
])
->descriptions([
'read' => trans('server/users.permissions.activity_read'),
]),
]),
]),
]),
]), ]),
]) ])
->modalHeading('Invite User') ->modalHeading('Invite User')

View File

@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets;
use App\Models\Server; use App\Models\Server;
use Carbon\Carbon; use Carbon\Carbon;
use Filament\Facades\Filament;
use Filament\Support\RawJs; use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget; use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number; use Illuminate\Support\Number;
@ -16,10 +17,19 @@ class ServerCpuChart extends ChartWidget
public ?Server $server = null; public ?Server $server = null;
public static function canView(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
}
protected function getData(): array protected function getData(): array
{ {
$period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
$cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute")) $cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))
->slice(-10) ->slice(-$period)
->map(fn ($value, $key) => [ ->map(fn ($value, $key) => [
'cpu' => Number::format($value, maxPrecision: 2), 'cpu' => Number::format($value, maxPrecision: 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), 'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),

View File

@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets;
use App\Models\Server; use App\Models\Server;
use Carbon\Carbon; use Carbon\Carbon;
use Filament\Facades\Filament;
use Filament\Support\RawJs; use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget; use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number; use Illuminate\Support\Number;
@ -16,9 +17,19 @@ class ServerMemoryChart extends ChartWidget
public ?Server $server = null; public ?Server $server = null;
public static function canView(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
}
protected function getData(): array protected function getData(): array
{ {
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->slice(-10) $period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))
->slice(-$period)
->map(fn ($value, $key) => [ ->map(fn ($value, $key) => [
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2), 'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'), 'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),

View File

@ -4,61 +4,72 @@ namespace App\Filament\Server\Widgets;
use App\Models\Server; use App\Models\Server;
use Carbon\Carbon; use Carbon\Carbon;
use Filament\Facades\Filament;
use Filament\Support\RawJs; use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget; use Filament\Widgets\ChartWidget;
class ServerNetworkChart extends ChartWidget class ServerNetworkChart extends ChartWidget
{ {
protected ?string $heading = 'Network'; protected static ?string $pollingInterval = '1s';
protected ?string $pollingInterval = '1s'; protected static ?string $maxHeight = '200px';
protected ?string $maxHeight = '300px';
public ?Server $server = null; public ?Server $server = null;
public static function canView(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
}
protected function getData(): array protected function getData(): array
{ {
$data = cache()->get("servers.{$this->server->id}.network"); $previous = null;
$rx = collect($data) $period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
->slice(-10) $net = collect(cache()->get("servers.{$this->server->id}.network"))
->map(fn ($value, $key) => [ ->slice(-$period)
'rx' => $value->rx_bytes, ->map(function ($current, $timestamp) use (&$previous) {
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'), $net = null;
])
->all();
$tx = collect($data) if ($previous !== null) {
->slice(-10) $net = [
->map(fn ($value, $key) => [ 'rx' => max(0, $current->rx_bytes - $previous->rx_bytes),
'tx' => $value->rx_bytes, 'tx' => max(0, $current->tx_bytes - $previous->tx_bytes),
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'), 'timestamp' => Carbon::createFromTimestamp($timestamp, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
]) ];
}
$previous = $current;
return $net;
})
->all(); ->all();
return [ return [
'datasets' => [ 'datasets' => [
[ [
'label' => 'Inbound', 'label' => 'Inbound',
'data' => array_column($rx, 'rx'), 'data' => array_column($net, 'rx'),
'backgroundColor' => [ 'backgroundColor' => [
'rgba(96, 165, 250, 0.3)', 'rgba(100, 255, 105, 0.5)',
], ],
'tension' => '0.3', 'tension' => '0.3',
'fill' => true, 'fill' => true,
], ],
[ [
'label' => 'Outbound', 'label' => 'Outbound',
'data' => array_column($tx, 'tx'), 'data' => array_column($net, 'tx'),
'backgroundColor' => [ 'backgroundColor' => [
'rgba(165, 96, 250, 0.3)', 'rgba(96, 165, 250, 0.3)',
], ],
'tension' => '0.3', 'tension' => '0.3',
'fill' => true, 'fill' => true,
], ],
], ],
'labels' => array_column($rx, 'timestamp'), 'labels' => array_column($net, 'timestamp'),
]; ];
} }
@ -69,25 +80,38 @@ class ServerNetworkChart extends ChartWidget
protected function getOptions(): RawJs protected function getOptions(): RawJs
{ {
// TODO: use "panel.use_binary_prefix" config value
return RawJs::make(<<<'JS' return RawJs::make(<<<'JS'
{ {
scales: { scales: {
x: { x: {
grid: { display: false,
display: false,
},
ticks: {
display: true,
},
display: false, //debug
}, },
y: { y: {
min: 0,
ticks: { ticks: {
display: true, display: true,
callback(value) {
const bytes = typeof value === 'string' ? parseInt(value, 10) : value;
if (bytes < 1) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const number = Number((bytes / Math.pow(1024, i)).toFixed(2));
return `${number} ${['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'][i]}`;
},
}, },
}, },
} }
} }
JS); JS);
} }
public function getHeading(): string
{
$lastData = collect(cache()->get("servers.{$this->server->id}.network"))->last();
return 'Network - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0);
}
} }

View File

@ -6,8 +6,10 @@ use App\Enums\ContainerStatus;
use App\Filament\Server\Components\SmallStatBlock; use App\Filament\Server\Components\SmallStatBlock;
use App\Models\Server; use App\Models\Server;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Filament\Notifications\Notification;
use Filament\Widgets\StatsOverviewWidget; use Filament\Widgets\StatsOverviewWidget;
use Illuminate\Support\Number; use Illuminate\Support\Number;
use Livewire\Attributes\On;
class ServerOverview extends StatsOverviewWidget class ServerOverview extends StatsOverviewWidget
{ {
@ -19,14 +21,10 @@ class ServerOverview extends StatsOverviewWidget
{ {
return [ return [
SmallStatBlock::make('Name', $this->server->name) SmallStatBlock::make('Name', $this->server->name)
->extraAttributes([ ->copyOnClick(fn () => request()->isSecure()),
'class' => 'overflow-x-auto',
]),
SmallStatBlock::make('Status', $this->status()), SmallStatBlock::make('Status', $this->status()),
SmallStatBlock::make('Address', $this->server->allocation->address) SmallStatBlock::make('Address', $this->server->allocation->address)
->extraAttributes([ ->copyOnClick(fn () => request()->isSecure()),
'class' => 'overflow-x-auto',
]),
SmallStatBlock::make('CPU', $this->cpuUsage()), SmallStatBlock::make('CPU', $this->cpuUsage()),
SmallStatBlock::make('Memory', $this->memoryUsage()), SmallStatBlock::make('Memory', $this->memoryUsage()),
SmallStatBlock::make('Disk', $this->diskUsage()), SmallStatBlock::make('Disk', $this->diskUsage()),
@ -93,4 +91,16 @@ class ServerOverview extends StatsOverviewWidget
return $used . ($this->server->disk > 0 ? ' / ' . $total : ' / ∞'); return $used . ($this->server->disk > 0 ? ' / ' . $total : ' / ∞');
} }
#[On('copyClick')]
public function copyClick(string $value): void
{
$this->js("window.navigator.clipboard.writeText('{$value}');");
Notification::make()
->title('Copied to clipboard')
->body($value)
->success()
->send();
}
} }

Some files were not shown because too many files have changed in this diff Show More