Relocate some actions to toolbarActions

This commit is contained in:
notCharles 2025-07-20 13:00:01 -04:00
parent 91fcaf4b45
commit b5f6cc345b
7 changed files with 371 additions and 394 deletions

View File

@ -15,6 +15,7 @@ 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\Services\Backups\InitiateBackupService;
use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
@ -23,6 +24,7 @@ use App\Traits\Filament\CanModifyTable;
use App\Traits\Filament\HasLimitBadge;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\Checkbox;
@ -34,12 +36,14 @@ use Filament\Notifications\Notification;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class BackupResource extends Resource
@ -122,18 +126,21 @@ class BackupResource extends Resource
->recordActions([
ActionGroup::make([
Action::make('lock')
->iconSize(IconSize::Large)
->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')
->iconSize(IconSize::Large)
->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')
->iconSize(IconSize::Large)
->color('success')
->icon('tabler-folder-up')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
@ -185,6 +192,7 @@ class BackupResource extends Resource
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
DeleteAction::make('delete')
->iconSize(IconSize::Large)
->disabled(fn (Backup $backup) => $backup->is_locked)
->modalDescription(fn (Backup $backup) => 'Do you wish to delete ' . $backup->name . '?')
->modalSubmitActionLabel('Delete Backup')
@ -207,7 +215,45 @@ class BackupResource extends Resource
->log();
})
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
]),
])->iconSize(IconSize::ExtraLarge),
])
->toolbarActions([
CreateAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
->icon('tabler-file-zip')
->tooltip(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup Limit Reached' : 'Create Backup')
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
->createAnother(false)
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
$action = $initiateBackupService->setIgnoredFiles(explode(PHP_EOL, $data['ignored'] ?? ''));
if (auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
$action->setIsLocked((bool) $data['is_locked']);
}
try {
$backup = $action->handle($server, $data['name']);
Activity::event('server:backup.start')
->subject($backup)
->property(['name' => $backup->name, 'locked' => (bool) $data['is_locked']])
->log();
return Notification::make()
->title('Backup Created')
->body($backup->name . ' created.')
->success()
->send();
} catch (HttpException $e) {
return Notification::make()
->danger()
->title('Backup Failed')
->body($e->getMessage() . ' Try again' . ($e->getHeaders()['Retry-After'] ? ' in ' . $e->getHeaders()['Retry-After'] . ' seconds.' : ''))
->send();
}
}),
]);
}

View File

@ -2,20 +2,12 @@
namespace App\Filament\Server\Resources\BackupResource\Pages;
use App\Facades\Activity;
use App\Filament\Server\Resources\BackupResource;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Backups\InitiateBackupService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ListBackups extends ListRecords
{
@ -27,44 +19,7 @@ class ListBackups extends ListRecords
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
CreateAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
->createAnother(false)
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
$action = $initiateBackupService->setIgnoredFiles(explode(PHP_EOL, $data['ignored'] ?? ''));
if (auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
$action->setIsLocked((bool) $data['is_locked']);
}
try {
$backup = $action->handle($server, $data['name']);
Activity::event('server:backup.start')
->subject($backup)
->property(['name' => $backup->name, 'locked' => (bool) $data['is_locked']])
->log();
return Notification::make()
->title('Backup Created')
->body($backup->name . ' created.')
->success()
->send();
} catch (HttpException $e) {
return Notification::make()
->danger()
->title('Backup Failed')
->body($e->getMessage() . ' Try again' . ($e->getHeaders()['Retry-After'] ? ' in ' . $e->getHeaders()['Retry-After'] . ' seconds.' : ''))
->send();
}
}),
];
}

View File

@ -18,6 +18,7 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
@ -34,6 +35,7 @@ use Filament\Resources\Pages\PageRegistration;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
@ -116,7 +118,7 @@ class ListFiles extends ListRecords
Action::make('view')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->label('Open')
->icon('tabler-eye')
->icon('tabler-eye')->iconSize(IconSize::Large)
->visible(fn (File $file) => $file->is_directory)
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
EditAction::make('edit')
@ -128,7 +130,7 @@ class ListFiles extends ListRecords
Action::make('rename')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label('Rename')
->icon('tabler-forms')
->icon('tabler-forms')->iconSize(IconSize::Large)
->schema([
TextInput::make('name')
->label('File name')
@ -156,7 +158,7 @@ class ListFiles extends ListRecords
Action::make('copy')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label('Copy')
->icon('tabler-copy')
->icon('tabler-copy')->iconSize(IconSize::Large)
->visible(fn (File $file) => $file->is_file)
->action(function (File $file) {
$this->getDaemonFileRepository()->copyFile(join_paths($this->path, $file->name));
@ -175,13 +177,13 @@ class ListFiles extends ListRecords
Action::make('download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->label('Download')
->icon('tabler-download')
->icon('tabler-download')->iconSize(IconSize::Large)
->visible(fn (File $file) => $file->is_file)
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
Action::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label('Move')
->icon('tabler-replace')
->icon('tabler-replace')->iconSize(IconSize::Large)
->schema([
TextInput::make('location')
->label('New location')
@ -216,7 +218,7 @@ class ListFiles extends ListRecords
Action::make('permissions')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label('Permissions')
->icon('tabler-license')
->icon('tabler-license')->iconSize(IconSize::Large)
->schema([
CheckboxList::make('owner')
->bulkToggleable()
@ -272,7 +274,7 @@ class ListFiles extends ListRecords
Action::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->label('Archive')
->icon('tabler-archive')
->icon('tabler-archive')->iconSize(IconSize::Large)
->schema([
TextInput::make('name')
->label('Archive name')
@ -299,7 +301,7 @@ class ListFiles extends ListRecords
Action::make('unarchive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->label('Unarchive')
->icon('tabler-archive')
->icon('tabler-archive')->iconSize(IconSize::Large)
->visible(fn (File $file) => $file->isArchive())
->action(function (File $file) {
$this->getDaemonFileRepository()->decompressFile($this->path, $file->name);
@ -316,11 +318,11 @@ class ListFiles extends ListRecords
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
]),
])->iconSize(IconSize::Large),
DeleteAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->label('')
->icon('tabler-trash')
->icon('tabler-trash')->iconSize(IconSize::Large)
->requiresConfirmation()
->modalHeading(fn (File $file) => trans('filament-actions::delete.single.modal.heading', ['label' => $file->name . ' ' . ($file->is_directory ? 'folder' : 'file')]))
->action(function (File $file) {
@ -333,77 +335,208 @@ class ListFiles extends ListRecords
->log();
}),
])
->groupedBulkActions([
BulkAction::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->schema([
TextInput::make('location')
->label('Directory')
->hint('Enter the new directory, relative to the current directory.')
->required()
->live(),
TextEntry::make('new_location')
->state(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
])
->action(function (Collection $files, $data) {
$location = rtrim($data['location'], '/');
->toolbarActions([
BulkActionGroup::make([
BulkAction::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->schema([
TextInput::make('location')
->label('Directory')
->hint('Enter the new directory, relative to the current directory.')
->required()
->live(),
TextEntry::make('new_location')
->state(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
])
->action(function (Collection $files, $data) {
$location = rtrim($data['location'], '/');
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
Activity::event('server:file.rename')
->property('directory', $this->path)
->property('files', $files)
->log();
Activity::event('server:file.rename')
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
->success()
->send();
}),
BulkAction::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
Notification::make()
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
->success()
->send();
}),
BulkAction::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->schema([
TextInput::make('name')
->label('Archive name')
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
])
->action(function ($data, Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
Activity::event('server:file.compress')
->property('name', $archive['name'])
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title('Archive created')
->body($archive['name'])
->success()
->send();
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->action(function (Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
Activity::event('server:file.delete')
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title(count($files) . ' Files deleted.')
->success()
->send();
}),
]),
Action::make('new_file')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->tooltip('New File')
->hiddenLabel()->icon('tabler-file-plus')->iconButton()->iconSize(IconSize::ExtraLarge)
->color('primary')
->keyBindings('')
->modalSubmitActionLabel('Create')
->action(function ($data) {
$path = join_paths($this->path, $data['name']);
try {
$this->getDaemonFileRepository()->putContent($path, $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', join_paths($path, $data['name']))
->log();
} catch (FileExistsException) {
AlertBanner::make()
->title('<code>' . $path . '</code> already exists!')
->danger()
->closable()
->send();
$this->redirect(self::getUrl(['path' => dirname($path)]));
}
})
->schema([
TextInput::make('name')
->label('Archive name')
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
])
->action(function ($data, Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
->label('Name')
->required(),
CodeEditor::make('editor')
->hiddenLabel(),
]),
Action::make('new_folder')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->hiddenLabel()->icon('tabler-folder-plus')->iconButton()->iconSize(IconSize::ExtraLarge)
->tooltip('New Folder')
->color('primary')
->action(function ($data) {
try {
$this->getDaemonFileRepository()->createDirectory($data['name'], $this->path);
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
Activity::event('server:file.create-directory')
->property(['directory' => $this->path, 'name' => $data['name']])
->log();
} catch (FileExistsException) {
$path = join_paths($this->path, $data['name']);
AlertBanner::make()
->title('<code>' . $path . '</code> already exists!')
->danger()
->closable()
->send();
Activity::event('server:file.compress')
->property('name', $archive['name'])
->property('directory', $this->path)
->property('files', $files)
->log();
$this->redirect(self::getUrl(['path' => dirname($path)]));
}
})
->schema([
TextInput::make('name')
->label('Folder Name')
->required(),
]),
Action::make('upload')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->hiddenLabel()->icon('tabler-upload')->iconButton()->iconSize(IconSize::ExtraLarge)
->tooltip('Upload')
->color('success')
->action(function ($data) {
if (count($data['files']) > 0 && !isset($data['url'])) {
/** @var UploadedFile $file */
foreach ($data['files'] as $file) {
$this->getDaemonFileRepository()->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
Notification::make()
->title('Archive created')
->body($archive['name'])
->success()
->send();
Activity::event('server:file.uploaded')
->property('directory', $this->path)
->property('file', $file->getClientOriginalName())
->log();
}
} elseif ($data['url'] !== null) {
$this->getDaemonFileRepository()->pull($data['url'], $this->path);
Activity::event('server:file.pull')
->property('url', $data['url'])
->property('directory', $this->path)
->log();
}
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->action(function (Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
Activity::event('server:file.delete')
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title(count($files) . ' Files deleted.')
->success()
->send();
}),
})
->schema([
Tabs::make()
->contained(false)
->schema([
Tab::make('Upload Files')
->live()
->schema([
FileUpload::make('files')
->storeFiles(false)
->previewable(false)
->preserveFilenames()
->maxSize((int) round($server->node->upload_size * (config('panel.use_binary_prefix') ? 1.048576 * 1024 : 1000)))
->multiple(),
]),
Tab::make('Upload From URL')
->live()
->disabled(fn (Get $get) => count($get('files')) > 0)
->schema([
TextInput::make('url')
->label('URL')
->url(),
]),
]),
]),
Action::make('search')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->tooltip('Global Search')
->color('primary')
->icon('tabler-world-search')
->modalSubmitActionLabel('Search')
->schema([
TextInput::make('searchTerm')
->placeholder('Enter a search term, e.g. *.txt')
->required()
->regex('/^[^*]*\*?[^*]*$/')
->minValue(3),
])
->action(fn ($data) => redirect(SearchFiles::getUrl([
'searchTerm' => $data['searchTerm'],
'path' => $this->path,
]))),
]);
}
@ -412,133 +545,7 @@ class ListFiles extends ListRecords
*/
protected function getDefaultHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
Action::make('new_file')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label('New File')
->color('gray')
->keyBindings('')
->modalSubmitActionLabel('Create')
->action(function ($data) {
$path = join_paths($this->path, $data['name']);
try {
$this->getDaemonFileRepository()->putContent($path, $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', join_paths($path, $data['name']))
->log();
} catch (FileExistsException) {
AlertBanner::make()
->title('<code>' . $path . '</code> already exists!')
->danger()
->closable()
->send();
$this->redirect(self::getUrl(['path' => dirname($path)]));
}
})
->schema([
TextInput::make('name')
->label('Name')
->required(),
CodeEditor::make('editor')
->hiddenLabel(),
]),
Action::make('new_folder')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label('New Folder')
->color('gray')
->action(function ($data) {
try {
$this->getDaemonFileRepository()->createDirectory($data['name'], $this->path);
Activity::event('server:file.create-directory')
->property(['directory' => $this->path, 'name' => $data['name']])
->log();
} catch (FileExistsException) {
$path = join_paths($this->path, $data['name']);
AlertBanner::make()
->title('<code>' . $path . '</code> already exists!')
->danger()
->closable()
->send();
$this->redirect(self::getUrl(['path' => dirname($path)]));
}
})
->schema([
TextInput::make('name')
->label('Folder Name')
->required(),
]),
Action::make('upload')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label('Upload')
->action(function ($data) {
if (count($data['files']) > 0 && !isset($data['url'])) {
/** @var UploadedFile $file */
foreach ($data['files'] as $file) {
$this->getDaemonFileRepository()->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
Activity::event('server:file.uploaded')
->property('directory', $this->path)
->property('file', $file->getClientOriginalName())
->log();
}
} elseif ($data['url'] !== null) {
$this->getDaemonFileRepository()->pull($data['url'], $this->path);
Activity::event('server:file.pull')
->property('url', $data['url'])
->property('directory', $this->path)
->log();
}
return redirect(ListFiles::getUrl(['path' => $this->path]));
})
->schema([
Tabs::make()
->contained(false)
->schema([
Tab::make('Upload Files')
->live()
->schema([
FileUpload::make('files')
->storeFiles(false)
->previewable(false)
->preserveFilenames()
->maxSize((int) round($server->node->upload_size * (config('panel.use_binary_prefix') ? 1.048576 * 1024 : 1000)))
->multiple(),
]),
Tab::make('Upload From URL')
->live()
->disabled(fn (Get $get) => count($get('files')) > 0)
->schema([
TextInput::make('url')
->label('URL')
->url(),
]),
]),
]),
Action::make('search')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->label('Global Search')
->modalSubmitActionLabel('Search')
->schema([
TextInput::make('searchTerm')
->placeholder('Enter a search term, e.g. *.txt')
->required()
->regex('/^[^*]*\*?[^*]*$/')
->minValue(3),
])
->action(fn ($data) => redirect(SearchFiles::getUrl([
'searchTerm' => $data['searchTerm'],
'path' => $this->path,
]))),
];
return [];
}
/**

View File

@ -2,6 +2,7 @@
namespace App\Filament\Server\Resources;
use App\Filament\Components\Actions\ImportScheduleAction;
use App\Filament\Server\Resources\ScheduleResource\Pages\ListSchedules;
use App\Filament\Server\Resources\ScheduleResource\Pages\CreateSchedule;
use App\Filament\Server\Resources\ScheduleResource\Pages\ViewSchedule;
@ -20,6 +21,7 @@ use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Carbon\Carbon;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
@ -38,6 +40,7 @@ use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Support\Exceptions\Halt;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
@ -334,6 +337,18 @@ class ScheduleResource extends Resource
->property('name', $schedule->name)
->log();
}),
])
->toolbarActions([
CreateAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-calendar-plus')
->color('primary')
->tooltip('New Schedule'),
ImportScheduleAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-import')
->color('success')
->tooltip('Import Schedule'),
]);
}

View File

@ -2,13 +2,11 @@
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Components\Actions\ImportScheduleAction;
use App\Filament\Server\Resources\ScheduleResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListSchedules extends ListRecords
@ -21,11 +19,7 @@ class ListSchedules extends ListRecords
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->label('New Schedule'),
ImportScheduleAction::make(),
];
return [];
}
public function getBreadcrumbs(): array

View File

@ -2,10 +2,12 @@
namespace App\Filament\Server\Resources;
use App\Facades\Activity;
use App\Filament\Server\Resources\UserResource\Pages\ListUsers;
use App\Models\Permission;
use App\Models\Server;
use App\Models\User;
use App\Services\Subusers\SubuserCreationService;
use App\Services\Subusers\SubuserDeletionService;
use App\Services\Subusers\SubuserUpdateService;
use App\Traits\Filament\BlockAccessInConflict;
@ -13,6 +15,8 @@ use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyTable;
use App\Traits\Filament\HasLimitBadge;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Facades\Filament;
use Filament\Actions\Action;
use Filament\Forms\Components\CheckboxList;
@ -22,12 +26,14 @@ use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Notifications\Notification;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Actions\EditAction;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@ -113,7 +119,7 @@ class UserResource extends Resource
return $table
->paginated(false)
->searchable(false)
// ->searchable(false) TODO toolbarActions do not render without the search bar :/
->columns([
ImageColumn::make('picture')
->visibleFrom('lg')
@ -221,7 +227,101 @@ class UserResource extends Resource
return $data;
}),
]);
])
->toolbarActions([
CreateAction::make('invite')
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-user-plus')
->tooltip('Invite User')
->createAnother(false)
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_CREATE, $server))
->schema([
Grid::make()
->columnSpanFull()
->columns([
'default' => 1,
'sm' => 1,
'md' => 5,
'lg' => 6,
])
->schema([
TextInput::make('email')
->email()
->inlineLabel()
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 4,
'lg' => 5,
])
->required(),
Actions::make([
Action::make('assignAll')
->label('Assign All')
->action(function (Set $set, Get $get) use ($permissionsArray) {
$permissions = $permissionsArray;
foreach ($permissions as $key => $value) {
$allValues = array_unique($value);
$set($key, $allValues);
}
}),
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
Tabs::make()
->columnSpanFull()
->schema($tabs),
]),
])
->modalHeading('Invite User')
->modalIcon('tabler-user-plus')
->modalSubmitActionLabel('Invite')
->successNotificationTitle(null)
->failureNotificationTitle(null)
->action(function (Action $action, array $data, SubuserCreationService $service) use ($server) {
$email = strtolower($data['email']);
$permissions = collect($data)
->forget('email')
->flatMap(fn ($permissions, $key) => collect($permissions)->map(fn ($permission) => "$key.$permission"))
->push(Permission::ACTION_WEBSOCKET_CONNECT)
->unique()
->all();
try {
$subuser = $service->handle($server, $email, $permissions);
Activity::event('server:subuser.create')
->subject($subuser->user)
->property([
'email' => $data['email'],
'permissions' => $permissions,
]);
Notification::make()
->title('User Invited!')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Failed')
->body($exception->getMessage())
->danger()
->send();
$action->failure();
return;
}
$action->success();
return redirect(self::getUrl(tenant: $server));
}), ]);
}
/** @return array<string, PageRegistration> */

View File

@ -2,28 +2,11 @@
namespace App\Filament\Server\Resources\UserResource\Pages;
use App\Facades\Activity;
use App\Filament\Server\Resources\UserResource;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Subusers\SubuserCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
@ -36,130 +19,7 @@ class ListUsers extends ListRecords
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
$tabs = [];
$permissionsArray = [];
foreach (Permission::permissionData() as $data) {
$options = [];
$descriptions = [];
foreach ($data['permissions'] as $permission) {
$options[$permission] = str($permission)->headline();
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
->icon($data['icon'])
->schema([
CheckboxList::make($data['name'])
->label('')
->bulkToggleable()
->columns(2)
->options($options)
->descriptions($descriptions),
]),
]);
}
return [
CreateAction::make('invite')
->label('Invite User')
->createAnother(false)
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_CREATE, $server))
->schema([
Grid::make()
->columnSpanFull()
->columns([
'default' => 1,
'sm' => 1,
'md' => 5,
'lg' => 6,
])
->schema([
TextInput::make('email')
->email()
->inlineLabel()
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 4,
'lg' => 5,
])
->required(),
Actions::make([
Action::make('assignAll')
->label('Assign All')
->action(function (Set $set, Get $get) use ($permissionsArray) {
$permissions = $permissionsArray;
foreach ($permissions as $key => $value) {
$allValues = array_unique($value);
$set($key, $allValues);
}
}),
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
Tabs::make()
->columnSpanFull()
->schema($tabs),
]),
])
->modalHeading('Invite User')
->modalSubmitActionLabel('Invite')
->successNotificationTitle(null)
->failureNotificationTitle(null)
->action(function (Action $action, array $data, SubuserCreationService $service) use ($server) {
$email = strtolower($data['email']);
$permissions = collect($data)
->forget('email')
->flatMap(fn ($permissions, $key) => collect($permissions)->map(fn ($permission) => "$key.$permission"))
->push(Permission::ACTION_WEBSOCKET_CONNECT)
->unique()
->all();
try {
$subuser = $service->handle($server, $email, $permissions);
Activity::event('server:subuser.create')
->subject($subuser->user)
->property([
'email' => $data['email'],
'permissions' => $permissions,
]);
Notification::make()
->title('User Invited!')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Failed')
->body($exception->getMessage())
->danger()
->send();
$action->failure();
return;
}
$action->success();
return redirect(self::getUrl(tenant: $server));
}),
];
return [];
}
public function getBreadcrumbs(): array