Compare commits

..

No commits in common. "main" and "v1.0.0-beta20" have entirely different histories.

150 changed files with 1930 additions and 2456 deletions

View File

@ -18,17 +18,6 @@ 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,7 +24,6 @@ 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.}';
@ -58,7 +57,6 @@ 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,6 +7,7 @@ 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;
@ -43,6 +44,8 @@ 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, ContainerStatus::Missing]); return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
} }
public function isRestartable(): bool public function isRestartable(): bool
@ -97,16 +97,18 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
return true; return true;
} }
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]); return !in_array($this, [ContainerStatus::Offline]);
} }
public function isStoppable(): bool public function isStoppable(): bool
{ {
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]); return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]);
} }
public function isKillable(): bool public function isKillable(): bool
{ {
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]); // [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created]
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
} }
} }

View File

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

View File

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

View File

@ -27,16 +27,8 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
}; };
} }
public function getColor(bool $hex = false): string public function getColor(): 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

@ -0,0 +1,11 @@
<?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

@ -0,0 +1,16 @@
<?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

@ -14,11 +14,8 @@ use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
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
{ {
@ -38,7 +35,7 @@ class GSLToken extends FeatureProvider
public function getId(): string public function getId(): string
{ {
return 'gsl_token'; return 'gsltoken';
} }
public function getAction(): Action public function getAction(): Action
@ -47,9 +44,7 @@ class GSLToken extends FeatureProvider
$server = Filament::getTenant(); $server = Filament::getTenant();
/** @var ServerVariable $serverVariable */ /** @var ServerVariable $serverVariable */
$serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) { $serverVariable = $server->serverVariables()->where('env_variable', 'STEAM_ACC')->first();
$query->where('env_variable', 'STEAM_ACC');
})->first();
return Action::make($this->getId()) return Action::make($this->getId())
->requiresConfirmation() ->requiresConfirmation()
@ -59,7 +54,7 @@ class GSLToken extends FeatureProvider
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->form([ ->form([
Placeholder::make('info') Placeholder::make('info')
->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.'))), ->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 completely.'),
TextInput::make('gsltoken') TextInput::make('gsltoken')
->label('GSL Token') ->label('GSL Token')
->rules([ ->rules([

View File

@ -8,11 +8,8 @@ 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 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\Actions;
use Filament\Forms\Components\Actions\Action as FormAction; use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Component; use Filament\Forms\Components\Component;
@ -47,12 +44,9 @@ use Illuminate\Support\Str;
*/ */
class Settings extends Page implements HasForms class Settings extends Page implements HasForms
{ {
use CanCustomizeHeaderActions, InteractsWithHeaderActions {
CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions;
}
use CanCustomizeHeaderWidgets;
use EnvironmentWriterTrait; use EnvironmentWriterTrait;
use InteractsWithForms; use InteractsWithForms;
use InteractsWithHeaderActions;
protected static ?string $navigationIcon = 'tabler-settings'; protected static ?string $navigationIcon = 'tabler-settings';
@ -636,6 +630,7 @@ class Settings extends Page implements HasForms
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state) ->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state)) ->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'))),
@ -646,6 +641,7 @@ class Settings extends Page implements HasForms
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state) ->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state)) ->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'))),
@ -797,8 +793,7 @@ class Settings extends Page implements HasForms
} }
} }
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Action::make('save') Action::make('save')

View File

@ -6,16 +6,11 @@ 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 Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Fieldset;
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\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Actions\DeleteAction;
@ -25,11 +20,6 @@ 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 $navigationIcon = 'tabler-key'; protected static ?string $navigationIcon = 'tabler-key';
@ -66,7 +56,7 @@ class ApiKeyResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
public static function defaultTable(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -89,7 +79,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', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null), ->url(fn (ApiKey $apiKey) => auth()->user()->can('update user', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
]) ])
->actions([ ->actions([
DeleteAction::make(), DeleteAction::make(),
@ -102,7 +92,7 @@ class ApiKeyResource extends Resource
]); ]);
} }
public static function defaultForm(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
@ -152,8 +142,7 @@ class ApiKeyResource extends Resource
]); ]);
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListApiKeys::route('/'), 'index' => Pages\ListApiKeys::route('/'),

View File

@ -4,24 +4,16 @@ 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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -4,22 +4,14 @@ 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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -3,19 +3,12 @@
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 Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Set; use Filament\Forms\Set;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\DeleteBulkAction;
@ -27,11 +20,6 @@ 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 $navigationIcon = 'tabler-database'; protected static ?string $navigationIcon = 'tabler-database';
@ -63,7 +51,7 @@ class DatabaseHostResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
public static function defaultTable(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -101,7 +89,7 @@ class DatabaseHostResource extends Resource
]); ]);
} }
public static function defaultForm(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
@ -162,16 +150,7 @@ class DatabaseHostResource extends Resource
]); ]);
} }
/** @return class-string<RelationManager>[] */ public static function getPages(): array
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('/'),
@ -185,10 +164,8 @@ class DatabaseHostResource extends Resource
{ {
$query = parent::getEloquentQuery(); $query = parent::getEloquentQuery();
return $query->where(function (Builder $query) { return $query->whereHas('nodes', function (Builder $query) {
return $query->whereHas('nodes', function (Builder $query) { $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')); })->orDoesntHave('nodes');
})->orDoesntHave('nodes');
});
} }
} }

View File

@ -4,8 +4,6 @@ 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 App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Placeholder;
@ -28,8 +26,6 @@ use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
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;

View File

@ -3,12 +3,9 @@
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;
@ -18,9 +15,6 @@ use PDOException;
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;
@ -30,8 +24,7 @@ class EditDatabaseHost extends EditRecord
$this->hostUpdateService = $hostUpdateService; $this->hostUpdateService = $hostUpdateService;
} }
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()
@ -46,6 +39,17 @@ class EditDatabaseHost extends EditRecord
return []; return [];
} }
public function getRelationManagers(): array
{
if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) {
return [
DatabasesRelationManager::class,
];
}
return [];
}
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,22 +4,14 @@ 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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -3,25 +3,29 @@
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\Traits\Filament\CanCustomizeHeaderActions; use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
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

@ -71,10 +71,10 @@ class DatabasesRelationManager extends RelationManager
]) ])
->actions([ ->actions([
DeleteAction::make() DeleteAction::make()
->authorize(fn (Database $database) => auth()->user()->can('delete', $database)), ->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
ViewAction::make() ViewAction::make()
->color('primary') ->color('primary')
->hidden(fn () => !auth()->user()->can('viewAny', Database::class)), ->hidden(fn () => !auth()->user()->can('viewList database')),
]); ]);
} }
} }

View File

@ -3,19 +3,11 @@
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 $navigationIcon = 'tabler-eggs'; protected static ?string $navigationIcon = 'tabler-eggs';
@ -52,16 +44,7 @@ class EggResource extends Resource
return ['name', 'tags', 'uuid', 'id']; return ['name', 'tags', 'uuid', 'id'];
} }
/** @return class-string<RelationManager>[] */ public static function getPages(): array
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

@ -6,10 +6,6 @@ use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
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\Fieldset; use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
@ -32,15 +28,11 @@ use Illuminate\Validation\Rules\Unique;
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -4,15 +4,12 @@ namespace App\Filament\Admin\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
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\Fieldset; use Filament\Forms\Components\Fieldset;
@ -34,9 +31,6 @@ use Illuminate\Validation\Rules\Unique;
class EditEgg extends EditRecord class EditEgg extends EditRecord
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = EggResource::class; protected static string $resource = EggResource::class;
public function form(Form $form): Form public function form(Form $form): Form
@ -257,8 +251,7 @@ class EditEgg extends EditRecord
]); ]);
} }
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()
@ -280,4 +273,11 @@ class EditEgg extends EditRecord
{ {
return []; return [];
} }
public function getRelationManagers(): array
{
return [
ServersRelationManager::class,
];
}
} }

View File

@ -10,10 +10,6 @@ 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 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\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\CreateAction;
@ -27,9 +23,6 @@ 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
@ -102,8 +95,7 @@ class ListEggs extends ListRecords
]); ]);
} }
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
ImportEggHeaderAction::make() ImportEggHeaderAction::make()

View File

@ -4,10 +4,6 @@ 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\Forms\Components\Group; use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
@ -15,7 +11,6 @@ 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\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\DeleteBulkAction;
@ -27,11 +22,6 @@ 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 $navigationIcon = 'tabler-layers-linked'; protected static ?string $navigationIcon = 'tabler-layers-linked';
@ -63,7 +53,7 @@ class MountResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
public static function defaultTable(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -86,7 +76,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.writable')), ->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writeable')),
]) ])
->actions([ ->actions([
ViewAction::make() ViewAction::make()
@ -104,7 +94,7 @@ class MountResource extends Resource
]); ]);
} }
public static function defaultForm(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
@ -172,8 +162,7 @@ class MountResource extends Resource
]); ]);
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListMounts::route('/'), 'index' => Pages\ListMounts::route('/'),
@ -187,10 +176,8 @@ class MountResource extends Resource
{ {
$query = parent::getEloquentQuery(); $query = parent::getEloquentQuery();
return $query->where(function (Builder $query) { return $query->whereHas('nodes', function (Builder $query) {
return $query->whereHas('nodes', function (Builder $query) { $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')); })->orDoesntHave('nodes');
})->orDoesntHave('nodes');
});
} }
} }

View File

@ -3,25 +3,17 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -3,22 +3,14 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make(), DeleteAction::make(),

View File

@ -4,22 +4,14 @@ 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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -3,22 +3,14 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),

View File

@ -5,18 +5,11 @@ 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; 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 $navigationIcon = 'tabler-server-2'; protected static ?string $navigationIcon = 'tabler-server-2';
@ -48,8 +41,7 @@ class NodeResource extends Resource
return (string) static::getEloquentQuery()->count() ?: null; return (string) static::getEloquentQuery()->count() ?: null;
} }
/** @return class-string<RelationManager>[] */ public static function getRelations(): array
public static function getDefaultRelations(): array
{ {
return [ return [
RelationManagers\AllocationsRelationManager::class, RelationManagers\AllocationsRelationManager::class,
@ -57,8 +49,7 @@ class NodeResource extends Resource
]; ];
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListNodes::route('/'), 'index' => Pages\ListNodes::route('/'),

View File

@ -4,8 +4,6 @@ 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 App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Forms; use Filament\Forms;
use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid; use Filament\Forms\Components\Grid;
@ -23,9 +21,6 @@ use Illuminate\Support\HtmlString;
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;
@ -129,10 +124,15 @@ class CreateNode extends CreateRecord
'lg' => 1, 'lg' => 1,
]), ]),
TextInput::make('daemon_connect') TextInput::make('daemon_listen')
->columnSpan(1) ->columnSpan([
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) 'default' => 1,
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help')) 'sm' => 1,
'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)
@ -193,21 +193,7 @@ class CreateNode extends CreateRecord
->afterStateUpdated(function ($state, Set $set) { ->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https'); $set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy'); $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'))
@ -423,13 +409,4 @@ 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,8 +8,6 @@ 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; use Filament\Actions;
use Filament\Forms; use Filament\Forms;
@ -36,9 +34,6 @@ use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
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;
@ -186,10 +181,10 @@ class EditNode extends EditRecord
false => 'danger', false => 'danger',
]) ])
->columnSpan(1), ->columnSpan(1),
TextInput::make('daemon_connect') TextInput::make('daemon_listen')
->columnSpan(1) ->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) ->label(trans('admin/node.port'))
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help')) ->helperText(trans('admin/node.port_help'))
->minValue(1) ->minValue(1)
->maxValue(65535) ->maxValue(65535)
->default(8080) ->default(8080)
@ -244,20 +239,7 @@ class EditNode extends EditRecord
->afterStateUpdated(function ($state, Set $set) { ->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https'); $set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy'); $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'))
@ -635,8 +617,7 @@ class EditNode extends EditRecord
return []; return [];
} }
/** @return array<Actions\Action|Actions\ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() Actions\DeleteAction::make()
@ -646,15 +627,6 @@ 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,8 +6,6 @@ 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\Actions;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\CreateAction;
@ -18,9 +16,6 @@ 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
@ -78,8 +73,7 @@ class ListNodes extends ListRecords
]); ]);
} }
/** @return array<Actions\Action|Actions\ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() Actions\CreateAction::make()

View File

@ -97,7 +97,7 @@ class AllocationsRelationManager extends RelationManager
]) ])
->groupedBulkActions([ ->groupedBulkActions([
DeleteBulkAction::make() DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())), ->authorize(fn () => auth()->user()->can('update node')),
]); ]);
} }
} }

View File

@ -3,6 +3,7 @@
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;
@ -15,34 +16,22 @@ 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
{ {
$sessionKey = "node_stats.{$this->node->id}"; $threads = $this->node->systemInformation()['cpu_count'] ?? 0;
$data = $this->node->statistics(); $cpu = collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))
->slice(-10)
$this->threads = session("{$sessionKey}.threads", $this->node->systemInformation()['cpu_count'] ?? 0); ->map(fn ($value, $key) => [
'cpu' => round($value * $threads, 2),
$this->cpuHistory = session("{$sessionKey}.cpu_history", []); 'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
$this->cpuHistory[] = [ ])
'cpu' => round($data['cpu_percent'] * $this->threads, 2), ->all();
'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($this->cpuHistory, 'cpu'), 'data' => array_column($cpu, 'cpu'),
'backgroundColor' => [ 'backgroundColor' => [
'rgba(96, 165, 250, 0.3)', 'rgba(96, 165, 250, 0.3)',
], ],
@ -50,7 +39,7 @@ class NodeCpuChart extends ChartWidget
'fill' => true, 'fill' => true,
], ],
], ],
'labels' => array_column($this->cpuHistory, 'timestamp'), 'labels' => array_column($cpu, 'timestamp'),
'locale' => auth()->user()->language ?? 'en', 'locale' => auth()->user()->language ?? 'en',
]; ];
} }
@ -80,10 +69,10 @@ class NodeCpuChart extends ChartWidget
public function getHeading(): string public function getHeading(): string
{ {
$data = array_slice(end($this->cpuHistory), -60); $threads = $this->node->systemInformation()['cpu_count'] ?? 0;
$cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language); $cpu = Number::format(collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))->last() * $threads, maxPrecision: 2, locale: auth()->user()->language);
$max = Number::format($this->threads * 100, locale: auth()->user()->language); $max = Number::format($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,6 +3,7 @@
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;
@ -15,36 +16,19 @@ 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
{ {
$sessionKey = "node_stats.{$this->node->id}"; $memUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->slice(-10)
->map(fn ($value, $key) => [
$data = $this->node->statistics(); 'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2),
'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($this->memoryHistory, 'memory'), 'data' => array_column($memUsed, 'memory'),
'backgroundColor' => [ 'backgroundColor' => [
'rgba(96, 165, 250, 0.3)', 'rgba(96, 165, 250, 0.3)',
], ],
@ -52,7 +36,7 @@ class NodeMemoryChart extends ChartWidget
'fill' => true, 'fill' => true,
], ],
], ],
'labels' => array_column($this->memoryHistory, 'timestamp'), 'labels' => array_column($memUsed, 'timestamp'),
'locale' => auth()->user()->language ?? 'en', 'locale' => auth()->user()->language ?? 'en',
]; ];
} }
@ -82,15 +66,16 @@ class NodeMemoryChart extends ChartWidget
public function getHeading(): string public function getHeading(): string
{ {
$latestMemoryUsed = array_slice(end($this->memoryHistory), -60); $latestMemoryUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->last();
$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['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB' ? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB'; : Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
$total = config('panel.use_binary_prefix') $total = config('panel.use_binary_prefix')
? Number::format($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB' ? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB'; : Number::format($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

@ -4,10 +4,6 @@ namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\RoleResource\Pages; use App\Filament\Admin\Resources\RoleResource\Pages;
use App\Models\Role; use App\Models\Role;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Component; use Filament\Forms\Components\Component;
@ -18,7 +14,6 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Get; use Filament\Forms\Get;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\DeleteBulkAction;
@ -31,11 +26,6 @@ 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 $navigationIcon = 'tabler-users-group'; protected static ?string $navigationIcon = 'tabler-users-group';
@ -67,7 +57,7 @@ class RoleResource extends Resource
return static::getModel()::count() ?: null; return static::getModel()::count() ?: null;
} }
public static function defaultTable(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -107,7 +97,7 @@ class RoleResource extends Resource
]); ]);
} }
public static function defaultForm(Form $form): Form public static function form(Form $form): Form
{ {
$permissionSections = []; $permissionSections = [];
@ -157,8 +147,6 @@ 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')) {
@ -210,8 +198,7 @@ class RoleResource extends Resource
]); ]);
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListRoles::route('/'), 'index' => Pages\ListRoles::route('/'),

View File

@ -4,10 +4,6 @@ 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;
@ -18,17 +14,13 @@ 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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -4,10 +4,6 @@ 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;
@ -19,9 +15,6 @@ 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;
@ -52,8 +45,7 @@ class EditRole extends EditRecord
$this->record->syncPermissions($permissionModels); $this->record->syncPermissions($permissionModels);
} }
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()

View File

@ -3,22 +3,14 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make(), CreateAction::make(),

View File

@ -3,22 +3,14 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),

View File

@ -3,23 +3,15 @@
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\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\Components\CheckboxList;
use Filament\Forms\Get; 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; 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 $navigationIcon = 'tabler-brand-docker'; protected static ?string $navigationIcon = 'tabler-brand-docker';
@ -74,16 +66,7 @@ class ServerResource extends Resource
->columnSpanFull(); ->columnSpanFull();
} }
/** @return class-string<RelationManager>[] */ public static function getPages(): array
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('/'),
@ -96,6 +79,8 @@ class ServerResource extends Resource
{ {
$query = parent::getEloquentQuery(); $query = parent::getEloquentQuery();
return $query->whereIn('node_id', auth()->user()->accessibleNodes()->pluck('id')); return $query->whereHas('node', function (Builder $query) {
$query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
});
} }
} }

View File

@ -11,8 +11,6 @@ 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\Forms; use Filament\Forms;
@ -47,9 +45,6 @@ use LogicException;
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;
@ -149,7 +144,6 @@ 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'))
@ -211,7 +205,6 @@ 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;

View File

@ -5,6 +5,7 @@ namespace App\Filament\Admin\Resources\ServerResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; 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;
@ -25,8 +26,6 @@ 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; use Filament\Actions;
@ -63,9 +62,6 @@ use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
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;
@ -690,7 +686,7 @@ class EditServer extends EditRecord
ServerResource::getMountCheckboxList($get), ServerResource::getMountCheckboxList($get),
]), ]),
Tab::make(trans('admin/server.databases')) Tab::make(trans('admin/server.databases'))
->hidden(fn () => !auth()->user()->can('viewAny', Database::class)) ->hidden(fn () => !auth()->user()->can('viewList database'))
->icon('tabler-database') ->icon('tabler-database')
->columns(4) ->columns(4)
->schema([ ->schema([
@ -714,14 +710,14 @@ class EditServer extends EditRecord
->hintAction( ->hintAction(
Action::make('Delete') Action::make('Delete')
->label(trans('filament-actions::delete.single.modal.actions.delete.label')) ->label(trans('filament-actions::delete.single.modal.actions.delete.label'))
->authorize(fn (Database $database) => auth()->user()->can('delete', $database)) ->authorize(fn (Database $database) => auth()->user()->can('delete database', $database))
->color('danger') ->color('danger')
->icon('tabler-trash') ->icon('tabler-trash')
->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(trans('filament-actions::delete.single.label')) ->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')])) ->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?')
->action(function (DatabaseManagementService $databaseManagementService, $record) { ->action(function (DatabaseManagementService $databaseManagementService, $record) {
$databaseManagementService->delete($record); $databaseManagementService->delete($record);
$this->fillForm(); $this->fillForm();
@ -767,7 +763,7 @@ class EditServer extends EditRecord
->columnSpan(4), ->columnSpan(4),
FormActions::make([ FormActions::make([
Action::make('createDatabase') Action::make('createDatabase')
->authorize(fn () => auth()->user()->can('create', Database::class)) ->authorize(fn () => auth()->user()->can('create database'))
->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')
@ -855,7 +851,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.notifications.error_connecting', ['node' => $server->node->name])) ->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->danger() ->danger()
->send(); ->send();
} }
@ -904,7 +900,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.notifications.error_connecting', ['node' => $server->node->name])) ->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->send(); ->send();
} }
}), }),
@ -926,7 +922,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.notifications.error_connecting', ['node' => $server->node->name])) ->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->send(); ->send();
} }
}), }),
@ -991,7 +987,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.notifications.error_connecting', ['node' => $server->node->name])) ->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
->danger() ->danger()
->send(); ->send();
} }
@ -1037,8 +1033,7 @@ class EditServer extends EditRecord
]; ];
} }
/** @return array<Actions\Action|Actions\ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = $this->getRecord(); $server = $this->getRecord();
@ -1070,7 +1065,7 @@ class EditServer extends EditRecord
} }
}) })
->hidden(fn () => $canForceDelete) ->hidden(fn () => $canForceDelete)
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)), ->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
Actions\Action::make('ForceDelete') Actions\Action::make('ForceDelete')
->color('danger') ->color('danger')
->label(trans('filament-actions::force-delete.single.label')) ->label(trans('filament-actions::force-delete.single.label'))
@ -1087,7 +1082,7 @@ class EditServer extends EditRecord
} }
}) })
->visible(fn () => $canForceDelete) ->visible(fn () => $canForceDelete)
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)), ->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
Actions\Action::make('console') Actions\Action::make('console')
->label(trans('admin/server.console')) ->label(trans('admin/server.console'))
->icon('tabler-terminal') ->icon('tabler-terminal')
@ -1141,6 +1136,13 @@ class EditServer extends EditRecord
return null; return null;
} }
public function getRelationManagers(): array
{
return [
AllocationsRelationManager::class,
];
}
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
{ {
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false); $containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);

View File

@ -5,8 +5,6 @@ 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\Tables\Actions\Action; use Filament\Tables\Actions\Action;
@ -19,9 +17,6 @@ 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
@ -73,13 +68,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')) // TODO: update to policy check (fn (Server $server) --> $server is empty) ->hidden(!auth()->user()->can('update server'))
->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')) // TODO: update to policy check (fn (Server $server) --> $server is empty) ->hidden(auth()->user()->can('update server'))
->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')
@ -106,8 +101,7 @@ class ListServers extends ListRecords
]); ]);
} }
/** @return array<Actions\Action|Actions\ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Actions\CreateAction::make() Actions\CreateAction::make()

View File

@ -6,16 +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;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\DeleteBulkAction; use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction; use Filament\Tables\Actions\EditAction;
@ -28,11 +22,6 @@ use Illuminate\Database\Eloquent\Builder;
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 $navigationIcon = 'tabler-users'; protected static ?string $navigationIcon = 'tabler-users';
@ -64,7 +53,7 @@ class UserResource extends Resource
return static::getModel()::count() ?: null; return static::getModel()::count() ?: null;
} }
public static function defaultTable(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -110,7 +99,7 @@ class UserResource extends Resource
]); ]);
} }
public static function defaultForm(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->columns(['default' => 1, 'lg' => 3]) ->columns(['default' => 1, 'lg' => 3])
@ -157,16 +146,14 @@ class UserResource extends Resource
]); ]);
} }
/** @return class-string<RelationManager>[] */ public static function getRelations(): array
public static function getDefaultRelations(): array
{ {
return [ return [
RelationManagers\ServersRelationManager::class, RelationManagers\ServersRelationManager::class,
]; ];
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListUsers::route('/'), 'index' => Pages\ListUsers::route('/'),

View File

@ -5,18 +5,11 @@ 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;
@ -28,8 +21,7 @@ class CreateUser extends CreateRecord
$this->service = $service; $this->service = $service;
} }
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -5,19 +5,12 @@ 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;
@ -27,8 +20,7 @@ class EditUser extends EditRecord
$this->service = $service; $this->service = $service;
} }
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make() DeleteAction::make()

View File

@ -3,22 +3,14 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make(), CreateAction::make(),

View File

@ -3,22 +3,14 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),

View File

@ -4,14 +4,9 @@ namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\WebhookResource\Pages; use App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Models\WebhookConfiguration; use App\Models\WebhookConfiguration;
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\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Actions\DeleteAction;
@ -23,11 +18,6 @@ use Filament\Tables\Table;
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 $navigationIcon = 'tabler-webhook'; protected static ?string $navigationIcon = 'tabler-webhook';
@ -59,7 +49,7 @@ class WebhookResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
public static function defaultTable(Table $table): Table public static function table(Table $table): Table
{ {
return $table return $table
->columns([ ->columns([
@ -85,7 +75,7 @@ class WebhookResource extends Resource
]); ]);
} }
public static function defaultForm(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->schema([ ->schema([
@ -108,8 +98,7 @@ class WebhookResource extends Resource
]); ]);
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListWebhookConfigurations::route('/'), 'index' => Pages\ListWebhookConfigurations::route('/'),

View File

@ -3,23 +3,15 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getCreateFormAction()->formId('form'), $this->getCreateFormAction()->formId('form'),

View File

@ -3,22 +3,14 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
DeleteAction::make(), DeleteAction::make(),

View File

@ -4,22 +4,14 @@ 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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
CreateAction::make() CreateAction::make()

View File

@ -3,22 +3,14 @@
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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
EditAction::make(), EditAction::make(),

View File

@ -2,13 +2,15 @@
namespace App\Filament\Admin\Widgets; namespace App\Filament\Admin\Widgets;
use Filament\Forms\Components\Actions\Action; use Filament\Actions\CreateAction;
use Filament\Forms\Components\Placeholder; use Filament\Widgets\Widget;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class CanaryWidget extends FormWidget class CanaryWidget extends Widget
{ {
protected static 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
@ -16,28 +18,15 @@ class CanaryWidget extends FormWidget
return config('app.version') === 'canary'; return config('app.version') === 'canary';
} }
public function form(Form $form): Form public function getViewData(): array
{ {
return $form return [
->schema([ 'actions' => [
Section::make(trans('admin/dashboard.sections.intro-developers.heading')) CreateAction::make()
->icon('tabler-code') ->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
->iconColor('primary') ->icon('tabler-brand-github')
->collapsible() ->url('https://github.com/pelican-dev/panel/issues', true),
->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

@ -1,16 +0,0 @@
<?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,34 +2,26 @@
namespace App\Filament\Admin\Widgets; namespace App\Filament\Admin\Widgets;
use Filament\Forms\Components\Actions\Action; use Filament\Actions\CreateAction;
use Filament\Forms\Components\Placeholder; use Filament\Widgets\Widget;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class HelpWidget extends FormWidget class HelpWidget extends Widget
{ {
protected static string $view = 'filament.admin.widgets.help-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 4; protected static ?int $sort = 4;
public function form(Form $form): Form public function getViewData(): array
{ {
return $form return [
->schema([ 'actions' => [
Section::make(trans('admin/dashboard.sections.intro-help.heading')) CreateAction::make()
->icon('tabler-question-mark') ->label(trans('admin/dashboard.sections.intro-help.button_docs'))
->iconColor('info') ->icon('tabler-speedboat')
->collapsible() ->url('https://pelican.dev/docs', true),
->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,13 +4,15 @@ 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\Forms\Components\Actions\Action; use Filament\Actions\CreateAction;
use Filament\Forms\Components\Placeholder; use Filament\Widgets\Widget;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class NoNodesWidget extends FormWidget class NoNodesWidget extends Widget
{ {
protected static 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
@ -18,25 +20,15 @@ class NoNodesWidget extends FormWidget
return Node::count() <= 0; return Node::count() <= 0;
} }
public function form(Form $form): Form public function getViewData(): array
{ {
return $form return [
->schema([ 'actions' => [
Section::make(trans('admin/dashboard.sections.intro-first-node.heading')) CreateAction::make()
->label(trans('admin/dashboard.sections.intro-first-node.button_label'))
->icon('tabler-server-2') ->icon('tabler-server-2')
->iconColor('primary') ->url(CreateNode::getUrl()),
->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,37 +2,27 @@
namespace App\Filament\Admin\Widgets; namespace App\Filament\Admin\Widgets;
use Filament\Forms\Components\Actions\Action; use Filament\Actions\CreateAction;
use Filament\Forms\Components\Placeholder; use Filament\Widgets\Widget;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class SupportWidget extends FormWidget class SupportWidget extends Widget
{ {
protected static string $view = 'filament.admin.widgets.support-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 3; protected static ?int $sort = 3;
public function form(Form $form): Form public function getViewData(): array
{ {
return $form return [
->schema([ 'actions' => [
Section::make(trans('admin/dashboard.sections.intro-support.heading')) CreateAction::make()
->icon('tabler-heart-filled') ->label(trans('admin/dashboard.sections.intro-support.button_donate'))
->iconColor('danger') ->icon('tabler-cash')
->collapsible() ->url('https://pelican.dev/donate', true)
->persistCollapsed() ->color('success'),
->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,13 +3,15 @@
namespace App\Filament\Admin\Widgets; namespace App\Filament\Admin\Widgets;
use App\Services\Helpers\SoftwareVersionService; use App\Services\Helpers\SoftwareVersionService;
use Filament\Forms\Components\Actions\Action; use Filament\Actions\CreateAction;
use Filament\Forms\Components\Placeholder; use Filament\Widgets\Widget;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class UpdateWidget extends FormWidget class UpdateWidget extends Widget
{ {
protected static 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;
@ -19,34 +21,19 @@ class UpdateWidget extends FormWidget
$this->softwareVersionService = $softwareVersionService; $this->softwareVersionService = $softwareVersionService;
} }
public function form(Form $form): Form public function getViewData(): array
{ {
$isLatest = $this->softwareVersionService->isLatestPanel(); return [
'version' => $this->softwareVersionService->currentPanelVersion(),
return $form 'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
->schema([ 'isLatest' => $this->softwareVersionService->isLatestPanel(),
$isLatest 'actions' => [
? Section::make(trans('admin/dashboard.sections.intro-no-update.heading')) CreateAction::make()
->icon('tabler-checkbox') ->label(trans('admin/dashboard.sections.intro-update-available.heading'))
->iconColor('success') ->icon('tabler-clipboard-text')
->schema([ ->url('https://pelican.dev/docs/panel/update', true)
Placeholder::make('') ->color('warning'),
->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

@ -9,15 +9,12 @@ use App\Filament\Server\Pages\Console;
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 App\Traits\Filament\CanCustomizeHeaderActions; use AymanAlhattami\FilamentContextMenu\Columns\ContextMenuTextColumn;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Components\Tab; use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\Alignment;
use Filament\Tables\Actions\Action; use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup; use Filament\Tables\Columns\ColumnGroup;
use Filament\Tables\Columns\Column;
use Filament\Tables\Columns\Layout\Stack; use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
@ -28,9 +25,6 @@ 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;
@ -44,73 +38,121 @@ class ListServers extends ListRecords
$this->daemonPowerRepository = new DaemonPowerRepository(); $this->daemonPowerRepository = new DaemonPowerRepository();
} }
/** @return Stack[] */ public function table(Table $table): Table
protected function gridColumns(): array
{ {
return [ $baseQuery = auth()->user()->accessibleServers();
Stack::make([
ServerEntryColumn::make('server_entry')
->searchable(['name']),
]),
];
}
/** @return Column[] */ $menuOptions = function (Server $server) {
protected function tableColumns(): array $status = $server->retrieveStatus();
{
return [ return [
TextColumn::make('condition') Action::make('start')
->label('Status') ->color('primary')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
->visible(fn () => $status->isStartable())
->dispatch('powerAction', ['server' => $server, 'action' => 'start'])
->icon('tabler-player-play-filled'),
Action::make('restart')
->color('gray')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->visible(fn () => $status->isRestartable())
->dispatch('powerAction', ['server' => $server, 'action' => 'restart'])
->icon('tabler-refresh'),
Action::make('stop')
->color('danger')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn () => $status->isStoppable())
->dispatch('powerAction', ['server' => $server, 'action' => 'stop'])
->icon('tabler-player-stop-filled'),
Action::make('kill')
->color('danger')
->tooltip('This can result in data corruption and/or data loss!')
->dispatch('powerAction', ['server' => $server, 'action' => 'kill'])
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn () => $status->isKillable())
->icon('tabler-alert-square'),
];
};
$viewOne = [
ContextMenuTextColumn::make('condition')
->label('')
->default('unknown')
->wrap()
->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())
TextColumn::make('name') ->contextMenuActions($menuOptions)
->label('Server') ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
->description(fn (Server $server) => $server->description) ];
->grow()
->searchable(), $viewTwo = [
TextColumn::make('allocation.address') ContextMenuTextColumn::make('name')
->label('')
->size('md')
->searchable()
->contextMenuActions($menuOptions)
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
ContextMenuTextColumn::make('allocation.address')
->label('') ->label('')
->badge() ->badge()
->visibleFrom('md') ->copyable(request()->isSecure())
->copyable(request()->isSecure()), ->contextMenuActions($menuOptions)
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
];
$viewThree = [
TextColumn::make('cpuUsage') TextColumn::make('cpuUsage')
->label('Resources') ->label('')
->icon('tabler-cpu') ->icon('tabler-cpu')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0)) ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0))
->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage)) ->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage))
->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') ->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))
->state(fn (Server $server) => $server->formatResource('memory_bytes')) ->state(fn (Server $server) => $server->formatResource('memory_bytes'))
->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') ->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($usingGrid ? $this->gridColumns() : $this->tableColumns()) ->columns(
->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null) (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid'
->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions()) : []) ? [
->actionsAlignment(Alignment::Center->value) Stack::make([
->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null) ServerEntryColumn::make('server_entry')
->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!')
@ -153,33 +195,36 @@ class ListServers extends ListRecords
]; ];
} }
protected function getResourceColor(Server $server, string $resource): ?string public 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->retrieveResources()['cpu_absolute'] ?? 0; $current = $server->resources()['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->retrieveResources()['memory_bytes'] ?? 0; $current = $server->resources()['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->retrieveResources()['disk_bytes'] ?? 0; $current = $server->resources()['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;
} }
@ -193,6 +238,7 @@ class ListServers extends ListRecords
} }
return null; return null;
} }
#[On('powerAction')] #[On('powerAction')]
@ -207,8 +253,6 @@ class ListServers extends ListRecords
->success() ->success()
->send(); ->send();
cache()->forget("servers.$server->uuid.status");
$this->redirect(self::getUrl(['activeTab' => $this->activeTab])); $this->redirect(self::getUrl(['activeTab' => $this->activeTab]));
} catch (ConnectionException) { } catch (ConnectionException) {
Notification::make() Notification::make()
@ -217,36 +261,4 @@ class ListServers extends ListRecords
->send(); ->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

@ -26,7 +26,7 @@ class RotateDatabasePasswordAction extends Action
$this->icon('tabler-refresh'); $this->icon('tabler-refresh');
$this->authorize(fn (Database $database) => auth()->user()->can('update', $database)); $this->authorize(fn (Database $database) => auth()->user()->can('update database', $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 = 'livewire.columns.server-entry-column'; protected string $view = 'tables.columns.server-entry-column';
} }

View File

@ -12,15 +12,11 @@ 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;
use chillerlan\QRCode\QROptions; use chillerlan\QRCode\QROptions;
use DateTimeZone; use DateTimeZone;
use Filament\Actions\Action as HeaderAction;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Actions; use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
@ -36,7 +32,6 @@ 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\Forms\Get; use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Auth\EditProfile as BaseEditProfile; use Filament\Pages\Auth\EditProfile as BaseEditProfile;
use Filament\Support\Colors\Color; use Filament\Support\Colors\Color;
@ -55,9 +50,6 @@ use Laravel\Socialite\Facades\Socialite;
*/ */
class EditProfile extends BaseEditProfile class EditProfile extends BaseEditProfile
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
private ToggleTwoFactorService $toggleTwoFactorService; private ToggleTwoFactorService $toggleTwoFactorService;
public function boot(ToggleTwoFactorService $toggleTwoFactorService): void public function boot(ToggleTwoFactorService $toggleTwoFactorService): void
@ -296,8 +288,6 @@ class EditProfile extends BaseEditProfile
); );
Activity::event('user:api-key.create') Activity::event('user:api-key.create')
->actor($user)
->subject($user)
->subject($token->accessToken) ->subject($token->accessToken)
->property('identifier', $token->accessToken->identifier) ->property('identifier', $token->accessToken->identifier)
->log(); ->log();
@ -393,12 +383,12 @@ class EditProfile extends BaseEditProfile
'monospace' => 'monospace', //default 'monospace' => 'monospace', //default
]; ];
if (!Storage::disk('public')->exists('fonts')) { if (!Storage::disk('public')->exists('storage/fonts')) {
Storage::disk('public')->makeDirectory('fonts'); Storage::disk('public')->makeDirectory('storage/fonts');
$this->fillForm(); $this->fillForm();
} }
foreach (Storage::disk('public')->allFiles('fonts') as $file) { foreach (Storage::disk('public')->allFiles('storage/fonts') as $file) {
$fileInfo = pathinfo($file); $fileInfo = pathinfo($file);
if ($fileInfo['extension'] === 'ttf') { if ($fileInfo['extension'] === 'ttf') {
@ -410,38 +400,30 @@ class EditProfile extends BaseEditProfile
}) })
->reactive() ->reactive()
->default('monospace') ->default('monospace')
->afterStateUpdated(fn ($state, Set $set) => $set('font_preview', $state)), ->afterStateUpdated(fn ($state, callable $set) => $set('font_preview', $state)),
Placeholder::make('font_preview') Placeholder::make('font_preview')
->label(trans('profile.font_preview')) ->label(trans('profile.font_preview'))
->columnSpan(2) ->columnSpan(2)
->content(function (Get $get) { ->content(function (Get $get) {
$fontName = $get('console_font') ?? 'monospace'; $fontName = $get('console_font') ?? 'monospace';
$fontSize = $get('console_font_size') . 'px'; $fontSize = $get('console_font_size') . 'px';
$style = <<<CSS $fontUrl = asset("storage/fonts/{$fontName}.ttf");
.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 return new HtmlString(<<<HTML
<style> <style>
{$style} @font-face {
</style> font-family: "CustomPreviewFont";
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span> src: url("$fontUrl");
HTML); }
.preview-text {
font-family: "CustomPreviewFont";
font-size: $fontSize;
margin-top: 10px;
display: block;
}
</style>
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
HTML);
}), }),
TextInput::make('console_graph_period') TextInput::make('console_graph_period')
->label(trans('profile.graph_period')) ->label(trans('profile.graph_period'))
@ -512,8 +494,7 @@ class EditProfile extends BaseEditProfile
return []; return [];
} }
/** @return array<HeaderAction|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
$this->getSaveFormAction()->formId('form'), $this->getSaveFormAction()->formId('form'),

View File

@ -2,10 +2,8 @@
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\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\Actions; use Filament\Forms\Components\Actions;
@ -56,37 +54,14 @@ class Login extends BaseLogin
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 = false; $isValidToken = $this->google2FA->verifyKey(
if (strlen($token) === $this->google2FA->getOneTimePasswordLength()) { $user->totp_secret,
$isValidToken = $this->google2FA->verifyKey( $token,
$user->totp_secret, Config::integer('panel.auth.2fa.window'),
$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
@ -133,9 +108,7 @@ class Login extends BaseLogin
{ {
return TextInput::make('2fa') return TextInput::make('2fa')
->label(trans('auth.two-factor-code')) ->label(trans('auth.two-factor-code'))
->hintIcon('tabler-question-mark') ->hidden(fn () => !$this->verifyTwoFactor)
->hintIconTooltip(trans('auth.two-factor-hint'))
->visible(fn () => $this->verifyTwoFactor)
->required() ->required()
->live(); ->live();
} }

View File

@ -2,27 +2,43 @@
namespace App\Filament\Server\Components; namespace App\Filament\Server\Components;
use Closure;
use Filament\Support\Concerns\EvaluatesClosures;
use Filament\Widgets\StatsOverviewWidget\Stat; use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
class SmallStatBlock extends Stat class SmallStatBlock extends Stat
{ {
use EvaluatesClosures; protected string|Htmlable $label;
protected bool|Closure $copyOnClick = false; protected $value;
public function copyOnClick(bool|Closure $copyOnClick = true): static public function label(string|Htmlable $label): static
{ {
$this->copyOnClick = $copyOnClick; $this->label = $label;
return $this; return $this;
} }
public function shouldCopyOnClick(): bool public function value($value): static
{ {
return $this->evaluate($this->copyOnClick); $this->value = $value;
return $this;
}
public function getLabel(): string|Htmlable
{
return $this->label;
}
public function getValue()
{
return value($this->value);
}
public function toHtml(): string
{
return $this->render()->render();
} }
public function render(): View public function render(): View

View File

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

View File

@ -14,11 +14,9 @@ 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\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\ActionSize; use Filament\Support\Enums\ActionSize;
use Filament\Widgets\Widget; use Filament\Widgets\Widget;
@ -27,7 +25,6 @@ use Livewire\Attributes\On;
class Console extends Page class Console extends Page
{ {
use CanCustomizeHeaderActions;
use InteractsWithActions; use InteractsWithActions;
protected static ?string $navigationIcon = 'tabler-brand-tabler'; protected static ?string $navigationIcon = 'tabler-brand-tabler';
@ -148,8 +145,7 @@ class Console extends Page
$this->cacheHeaderActions(); $this->cacheHeaderActions();
} }
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();

View File

@ -3,9 +3,6 @@
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\Forms\Form; use Filament\Forms\Form;
@ -17,9 +14,6 @@ use Filament\Pages\Page;
*/ */
abstract class ServerFormPage extends Page abstract class ServerFormPage extends Page
{ {
use BlockAccessInConflict;
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use InteractsWithFormActions; use InteractsWithFormActions;
use InteractsWithForms; use InteractsWithForms;
@ -70,4 +64,17 @@ 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,40 +2,19 @@
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';
@ -46,96 +25,12 @@ class ActivityResource extends Resource
protected static ?string $navigationIcon = 'tabler-stack'; protected static ?string $navigationIcon = 'tabler-stack';
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)->where('subject_type', $server->getMorphClass())) return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id))
->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
@ -161,8 +56,7 @@ class ActivityResource extends Resource
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListActivities::route('/'), 'index' => Pages\ListActivities::route('/'),

View File

@ -2,18 +2,114 @@
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\Traits\Filament\CanCustomizeHeaderActions; use App\Models\ActivityLog;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Models\Server;
use App\Models\User;
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\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Tables\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())
->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 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,32 +2,16 @@
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';
@ -38,59 +22,17 @@ class AllocationResource extends Resource
protected static ?string $navigationIcon = 'tabler-network'; protected static ?string $navigationIcon = 'tabler-network';
public static function defaultTable(Table $table): Table // TODO: find better way handle server conflict state
public static function canAccess(): bool
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return $table if ($server->isInConflictState()) {
->columns([ return false;
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') return parent::canAccess();
->subject($allocation)
->property('allocation', $allocation->address)
->log();
}),
]);
} }
public static function canViewAny(): bool public static function canViewAny(): bool
@ -113,8 +55,7 @@ class AllocationResource extends Resource
return auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant());
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListAllocations::route('/'), 'index' => Pages\ListAllocations::route('/'),

View File

@ -4,31 +4,85 @@ 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 App\Traits\Filament\CanCustomizeHeaderActions; use Filament\Actions;
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\Tables\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;
/** @return array<Action|ActionGroup> */ public function table(Table $table): Table
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 [
Action::make('addAllocation') Actions\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'))
@ -39,7 +93,7 @@ class ListAllocations extends ListRecords
Activity::event('server:allocation.create') Activity::event('server:allocation.create')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->address) ->property('allocation', $allocation->toString())
->log(); ->log();
}), }),
]; ];

View File

@ -2,54 +2,16 @@
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;
@ -58,151 +20,45 @@ class BackupResource extends Resource
protected static bool $canCreateAnother = false; protected static bool $canCreateAnother = false;
protected static function getBadgeCount(): int public const WARNING_THRESHOLD = 0.7;
public static function getNavigationBadge(): string
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return $server->backups->count(); $limit = $server->backup_limit;
return $server->backups->count() . ($limit === 0 ? '' : ' / ' . $limit);
} }
protected static function getBadgeLimit(): int public static function getNavigationBadgeColor(): ?string
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return $server->backup_limit; $limit = $server->backup_limit;
$count = $server->backups->count();
if ($limit === 0) {
return null;
}
return $count >= $limit ? 'danger'
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
} }
public static function defaultForm(Form $form): Form // TODO: find better way handle server conflict state
{ 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();
return $table if ($server->isInConflictState()) {
->columns([ return false;
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();
}
if (!$backup->is_successful && is_null($backup->completed_at)) { return parent::canAccess();
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
@ -220,8 +76,7 @@ class BackupResource extends Resource
return auth()->user()->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant());
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListBackups::route('/'), 'index' => Pages\ListBackups::route('/'),

View File

@ -2,36 +2,165 @@
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\Traits\Filament\CanCustomizeHeaderActions; use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Filament\Components\Tables\Columns\DateTimeColumn;
use Filament\Actions\Action; use Filament\Actions;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
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\Notifications\Notification;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
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\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
class ListBackups extends ListRecords class ListBackups extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = BackupResource::class; protected static string $resource = BackupResource::class;
/** @return array<Action|ActionGroup> */ protected static bool $canCreateAnother = false;
protected function getDefaultHeaderActions(): array
public function form(Form $form): Form
{
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 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))
->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();
}
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 [
CreateAction::make() Actions\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,118 +2,62 @@
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 $navigationIcon = 'tabler-database'; protected static ?string $navigationIcon = 'tabler-database';
protected static function getBadgeCount(): int public const WARNING_THRESHOLD = 0.7;
public static function getNavigationBadge(): string
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return $server->databases->count(); $limit = $server->database_limit;
return $server->databases->count() . ($limit === 0 ? '' : ' / ' . $limit);
} }
protected static function getBadgeLimit(): int public static function getNavigationBadgeColor(): ?string
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return $server->database_limit; $limit = $server->database_limit;
$count = $server->databases->count();
if ($limit === 0) {
return null;
}
return $count >= $limit
? 'danger'
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
} }
public static function defaultForm(Form $form): Form // TODO: find better way handle server conflict state
public static function canAccess(): bool
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return $form if ($server->isInConflictState()) {
->schema([ return false;
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),
]);
}
public static function defaultTable(Table $table): Table return parent::canAccess();
{
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
@ -141,8 +85,7 @@ class DatabaseResource extends Resource
return auth()->user()->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant());
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListDatabases::route('/'), 'index' => Pages\ListDatabases::route('/'),

View File

@ -2,30 +2,90 @@
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\Forms\Components\Grid; use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class ListDatabases extends ListRecords class ListDatabases extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseResource::class; protected static string $resource = DatabaseResource::class;
/** @return array<Action|ActionGroup> */ public function form(Form $form): Form
protected function getDefaultHeaderActions(): array {
/** @var Server $server */
$server = Filament::getTenant();
return $form
->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),
]);
}
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,26 +5,32 @@ 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\Traits\Filament\BlockAccessInConflict; use App\Models\Server;
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;
protected static ?string $navigationIcon = 'tabler-files'; protected static ?string $navigationIcon = 'tabler-files';
// 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());
@ -45,8 +51,7 @@ class FileResource extends Resource
return auth()->user()->can(Permission::ACTION_FILE_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_FILE_DELETE, Filament::getTenant());
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'edit' => Pages\EditFiles::route('/edit/{path}'), 'edit' => Pages\EditFiles::route('/edit/{path}'),

View File

@ -12,8 +12,6 @@ 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\Forms\Components\Actions\Action; use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section; use Filament\Forms\Components\Section;
@ -39,8 +37,6 @@ use Livewire\Attributes\Locked;
*/ */
class EditFiles extends Page class EditFiles extends Page
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use InteractsWithFormActions; use InteractsWithFormActions;
use InteractsWithForms; use InteractsWithForms;

View File

@ -12,10 +12,7 @@ use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository; use App\Repositories\Daemon\DaemonFileRepository;
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\Actions\Action as HeaderAction; use Filament\Actions\Action as HeaderAction;
use Filament\Actions\ActionGroup as HeaderActionGroup;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
@ -46,9 +43,6 @@ 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]
@ -320,9 +314,9 @@ class ListFiles extends ListRecords
->label('') ->label('')
->icon('tabler-trash') ->icon('tabler-trash')
->requiresConfirmation() ->requiresConfirmation()
->modalHeading(fn (File $file) => trans('filament-actions::delete.single.modal.heading', ['label' => $file->name . ' ' . ($file->is_directory ? 'folder' : 'file')])) ->modalDescription(fn (File $file) => $file->name)
->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')
@ -405,8 +399,7 @@ class ListFiles extends ListRecords
]); ]);
} }
/** @return array<HeaderAction|HeaderActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();

View File

@ -7,8 +7,6 @@ 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;
@ -18,9 +16,6 @@ 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';

View File

@ -2,18 +2,12 @@
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\Traits\Filament\BlockAccessInConflict; use App\Models\Server;
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\Facades\Filament; use Filament\Facades\Filament;
@ -27,32 +21,31 @@ use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Set; use Filament\Forms\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\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;
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 $navigationIcon = 'tabler-clock'; protected static ?string $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());
@ -73,7 +66,7 @@ class ScheduleResource extends Resource
return auth()->user()->can(Permission::ACTION_SCHEDULE_DELETE, Filament::getTenant()); return auth()->user()->can(Permission::ACTION_SCHEDULE_DELETE, Filament::getTenant());
} }
public static function defaultForm(Form $form): Form public static function form(Form $form): Form
{ {
return $form return $form
->columns([ ->columns([
@ -310,54 +303,14 @@ class ScheduleResource extends Resource
]); ]);
} }
public static function defaultTable(Table $table): Table public static function getRelations(): 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();
}),
]);
}
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{ {
return [ return [
TasksRelationManager::class, TasksRelationManager::class,
]; ];
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListSchedules::route('/'), 'index' => Pages\ListSchedules::route('/'),

View File

@ -6,16 +6,11 @@ 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

@ -5,16 +5,11 @@ namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Facades\Activity; 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\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions; use Filament\Actions;
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
@ -40,8 +35,7 @@ class EditSchedule extends EditRecord
return $data; return $data;
} }
/** @return array<Actions\Action|Actions\ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Actions\DeleteAction::make() Actions\DeleteAction::make()

View File

@ -2,27 +2,65 @@
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\Traits\Filament\CanCustomizeHeaderActions; use App\Models\Schedule;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Filament\Components\Tables\Columns\DateTimeColumn;
use Filament\Actions\Action; use Filament\Actions;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
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;
class ListSchedules extends ListRecords class ListSchedules extends ListRecords
{ {
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ScheduleResource::class; protected static string $resource = ScheduleResource::class;
/** @return array<Action|ActionGroup> */ public function table(Table $table): Table
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 [
CreateAction::make() Actions\CreateAction::make()->label('New Schedule'),
->label('New Schedule'),
]; ];
} }

View File

@ -7,26 +7,18 @@ 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 Filament\Actions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Resources\Pages\ViewRecord; 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;
/** @return array<Action|ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
return [ return [
Action::make('runNow') Actions\Action::make('runNow')
->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant())) ->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant()))
->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now')) ->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now'))
->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary') ->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary')
@ -41,7 +33,7 @@ class ViewSchedule extends ViewRecord
$this->fillForm(); $this->fillForm();
}), }),
EditAction::make(), Actions\EditAction::make(),
]; ];
} }

View File

@ -8,11 +8,6 @@ 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\Forms\Components\Actions; use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Actions\Action;
@ -24,7 +19,6 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Set; use Filament\Forms\Set;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\DeleteAction; use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction; use Filament\Tables\Actions\EditAction;
@ -35,12 +29,6 @@ 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;
@ -49,12 +37,25 @@ class UserResource extends Resource
protected static ?string $tenantOwnershipRelationshipName = 'subServers'; protected static ?string $tenantOwnershipRelationshipName = 'subServers';
protected static function getBadgeCount(): int public static function getNavigationBadge(): string
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
return $server->subusers->count(); return (string) $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
@ -77,7 +78,7 @@ 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 defaultTable(Table $table): Table public static function table(Table $table): Table
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
@ -224,8 +225,7 @@ class UserResource extends Resource
]); ]);
} }
/** @return array<string, PageRegistration> */ public static function getPages(): array
public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListUsers::route('/'), 'index' => Pages\ListUsers::route('/'),

View File

@ -7,8 +7,6 @@ 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; use Filament\Actions;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -27,13 +25,9 @@ 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;
/** @return array<Actions\Action|Actions\ActionGroup> */ protected function getHeaderActions(): array
protected function getDefaultHeaderActions(): array
{ {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();

View File

@ -6,10 +6,8 @@ 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
{ {
@ -21,10 +19,14 @@ class ServerOverview extends StatsOverviewWidget
{ {
return [ return [
SmallStatBlock::make('Name', $this->server->name) SmallStatBlock::make('Name', $this->server->name)
->copyOnClick(fn () => request()->isSecure()), ->extraAttributes([
'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)
->copyOnClick(fn () => request()->isSecure()), ->extraAttributes([
'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()),
@ -91,16 +93,4 @@ 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();
}
} }

View File

@ -62,7 +62,7 @@ class NetworkAllocationController extends ClientApiController
if ($original !== $allocation->notes) { if ($original !== $allocation->notes) {
Activity::event('server:allocation.notes') Activity::event('server:allocation.notes')
->subject($allocation) ->subject($allocation)
->property(['allocation' => $allocation->address, 'old' => $original, 'new' => $allocation->notes]) ->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes])
->log(); ->log();
} }
@ -87,7 +87,7 @@ class NetworkAllocationController extends ClientApiController
Activity::event('server:allocation.primary') Activity::event('server:allocation.primary')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->address) ->property('allocation', $allocation->toString())
->log(); ->log();
return $this->fractal->item($allocation) return $this->fractal->item($allocation)
@ -114,7 +114,7 @@ class NetworkAllocationController extends ClientApiController
Activity::event('server:allocation.create') Activity::event('server:allocation.create')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->address) ->property('allocation', $allocation->toString())
->log(); ->log();
return $this->fractal->item($allocation) return $this->fractal->item($allocation)
@ -148,7 +148,7 @@ class NetworkAllocationController extends ClientApiController
Activity::event('server:allocation.delete') Activity::event('server:allocation.delete')
->subject($allocation) ->subject($allocation)
->property('allocation', $allocation->address) ->property('allocation', $allocation->toString())
->log(); ->log();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);

View File

@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api\Client\Servers;
use App\Facades\Activity; use App\Facades\Activity;
use App\Http\Controllers\Api\Client\ClientApiController; use App\Http\Controllers\Api\Client\ClientApiController;
use App\Http\Requests\Api\Client\Servers\Settings\DescriptionServerRequest;
use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest; use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
use App\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest; use App\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest;
use App\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest; use App\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest;
@ -34,39 +33,25 @@ class SettingsController extends ClientApiController
*/ */
public function rename(RenameServerRequest $request, Server $server): JsonResponse public function rename(RenameServerRequest $request, Server $server): JsonResponse
{ {
$originalName = $server->name;
$name = $request->input('name'); $name = $request->input('name');
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
$server->update(['name' => $name]); if ($server->name !== $name) {
if ($server->wasChanged('name')) {
Activity::event('server:settings.rename') Activity::event('server:settings.rename')
->property(['old' => $originalName, 'new' => $name]) ->property(['old' => $server->name, 'new' => $name])
->log(); ->log();
$server->name = $name;
} }
return new JsonResponse([], Response::HTTP_NO_CONTENT); if ($server->description !== $description && config('panel.editable_server_descriptions')) {
}
/**
* Update server description
*/
public function description(DescriptionServerRequest $request, Server $server): JsonResponse
{
if (!config('panel.editable_server_descriptions')) {
return new JsonResponse([], Response::HTTP_FORBIDDEN);
}
$originalDescription = $server->description;
$description = $request->input('description');
$server->update(['description' => $description ?? '']);
if ($server->wasChanged('description')) {
Activity::event('server:settings.description') Activity::event('server:settings.description')
->property(['old' => $originalDescription, 'new' => $description]) ->property(['old' => $server->description, 'new' => $description])
->log(); ->log();
$server->description = $description;
} }
$server->save();
return new JsonResponse([], Response::HTTP_NO_CONTENT); return new JsonResponse([], Response::HTTP_NO_CONTENT);
} }
@ -95,7 +80,7 @@ class SettingsController extends ClientApiController
*/ */
public function dockerImage(SetDockerImageRequest $request, Server $server): JsonResponse public function dockerImage(SetDockerImageRequest $request, Server $server): JsonResponse
{ {
if (!in_array($server->image, $server->egg->docker_images)) { if (!in_array($server->image, array_values($server->egg->docker_images))) {
throw new BadRequestHttpException('This server\'s Docker image has been manually set by an administrator and cannot be updated.'); throw new BadRequestHttpException('This server\'s Docker image has been manually set by an administrator and cannot be updated.');
} }

View File

@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests\Api\Client\Servers\Settings;
use App\Contracts\Http\ClientPermissionsRequest;
use App\Http\Requests\Api\Client\ClientApiRequest;
use App\Models\Permission;
class DescriptionServerRequest extends ClientApiRequest implements ClientPermissionsRequest
{
/**
* Returns the permissions string indicating which permission should be used to
* validate that the authenticated user has permission to perform this action against
* the given resource (server).
*/
public function permission(): string
{
return Permission::ACTION_SETTINGS_DESCRIPTION;
}
/**
* The rules to apply when validating this request.
*/
public function rules(): array
{
return [
'description' => 'string|nullable',
];
}
}

View File

@ -26,6 +26,7 @@ class RenameServerRequest extends ClientApiRequest implements ClientPermissionsR
{ {
return [ return [
'name' => Server::getRules()['name'], 'name' => Server::getRules()['name'],
'description' => 'string|nullable',
]; ];
} }
} }

View File

@ -58,7 +58,7 @@ abstract class SubuserRequest extends ClientApiRequest
$server = $this->route()->parameter('server'); $server = $this->route()->parameter('server');
// If we are an admin or the server owner, no need to perform these checks. // If we are an admin or the server owner, no need to perform these checks.
if ($user->can('update', $server) || $user->id === $server->owner_id) { if ($user->can('update server', $server) || $user->id === $server->owner_id) {
return; return;
} }

View File

@ -0,0 +1,35 @@
<?php
namespace App\Jobs;
use App\Models\Node;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class NodeStatistics implements ShouldBeUnique, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
foreach (Node::all() as $node) {
$stats = $node->statistics();
$timestamp = now()->getTimestamp();
foreach ($stats as $key => $value) {
$cacheKey = "nodes.{$node->id}.$key";
$data = cache()->get($cacheKey, []);
// Add current timestamp and value to the data array
$data[$timestamp] = $value;
// Update the cache with the new data, expires in 1 minute
cache()->put($cacheKey, $data, now()->addMinute());
}
}
}
}

View File

@ -4,7 +4,7 @@ namespace App\Listeners\Auth;
use App\Facades\Activity; use App\Facades\Activity;
use Illuminate\Auth\Events\Failed; use Illuminate\Auth\Events\Failed;
use Illuminate\Auth\Events\Login; use App\Events\Auth\DirectLogin;
class AuthenticationListener class AuthenticationListener
{ {
@ -12,10 +12,9 @@ class AuthenticationListener
* Handles an authentication event by logging the user and information about * Handles an authentication event by logging the user and information about
* the request. * the request.
*/ */
public function handle(Failed|Login $event): void public function handle(Failed|DirectLogin $event): void
{ {
$activity = Activity::withRequestMetadata(); $activity = Activity::withRequestMetadata();
if ($event->user) { if ($event->user) {
$activity = $activity->subject($event->user); $activity = $activity->subject($event->user);
} }

View File

@ -2,14 +2,22 @@
namespace App\Listeners\Auth; namespace App\Listeners\Auth;
use Illuminate\Http\Request;
use App\Facades\Activity; use App\Facades\Activity;
use Illuminate\Auth\Events\PasswordReset; use Illuminate\Auth\Events\PasswordReset;
class PasswordResetListener class PasswordResetListener
{ {
protected Request $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function handle(PasswordReset $event): void public function handle(PasswordReset $event): void
{ {
Activity::event('auth:password-reset') Activity::event('event:password-reset')
->withRequestMetadata() ->withRequestMetadata()
->subject($event->user) ->subject($event->user)
->log(); ->log();

View File

@ -1,64 +0,0 @@
<?php
namespace App\Livewire;
use App\Models\Server;
use Illuminate\View\View;
use Livewire\Component;
class ServerEntry extends Component
{
public Server $server;
public function render(): View
{
return view('livewire.server-entry');
}
public function placeholder(): string
{
return <<<'HTML'
<div class="relative">
<div
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
style="background-color: #D97706;">
</div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
<div class="flex items-center mb-5 gap-2">
<x-filament::loading-indicator class="h-5 w-5" />
<h2 class="text-xl font-bold">
{{ $server->name }}
</h2>
</div>
<div class="flex justify-between text-center">
<div>
<p class="text-sm dark:text-gray-400">CPU</p>
<p class="text-md font-semibold">{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">Memory</p>
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('memory', limit: true) }}</p>
</div>
<div>
<p class="text-sm dark:text-gray-400">Disk</p>
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('disk', limit: true) }}</p>
</div>
<div class="hidden sm:block">
<p class="text-sm dark:text-gray-400">Network</p>
<hr class="p-0.5">
<p class="text-md font-semibold">{{ $server->allocation->address }} </p>
</div>
</div>
</div>
</div>
HTML;
}
}

View File

@ -121,6 +121,11 @@ class Allocation extends Model
); );
} }
public function toString(): string
{
return $this->address;
}
/** /**
* Gets information for the server associated with this allocation. * Gets information for the server associated with this allocation.
*/ */

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