Merge branch 'main' into boy132/customizable-resources

This commit is contained in:
Boy132 2025-05-22 08:49:02 +02:00
commit a378bb16c5
15 changed files with 523 additions and 530 deletions

View File

@ -85,7 +85,7 @@ class MountResource extends Resource
->badge()
->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing')
->color(fn ($state) => $state ? 'success' : 'warning')
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writeable')),
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
])
->actions([
ViewAction::make()

View File

@ -2,6 +2,8 @@
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\Models\ActivityLog;
use App\Models\Permission;
@ -11,9 +13,20 @@ use App\Models\User;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
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\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\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\HtmlString;
class ActivityResource extends Resource
{
@ -30,6 +43,90 @@ class ActivityResource extends Resource
protected static ?string $navigationIcon = 'tabler-stack';
public static 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 static function getEloquentQuery(): Builder
{
/** @var Server $server */

View File

@ -2,114 +2,13 @@
namespace App\Filament\Server\Resources\ActivityResource\Pages;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
use App\Filament\Server\Resources\ActivityResource;
use App\Models\ActivityLog;
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\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
{
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
{
return [];

View File

@ -2,6 +2,7 @@
namespace App\Filament\Server\Resources;
use App\Facades\Activity;
use App\Filament\Server\Resources\AllocationResource\Pages;
use App\Models\Allocation;
use App\Models\Permission;
@ -10,6 +11,11 @@ use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Facades\Filament;
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;
class AllocationResource extends Resource
@ -28,6 +34,61 @@ class AllocationResource extends Resource
protected static ?string $navigationIcon = 'tabler-network';
public static function table(Table $table): Table
{
/** @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();
}),
]);
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_ALLOCATION_READ, Filament::getTenant());

View File

@ -4,85 +4,24 @@ namespace App\Filament\Server\Resources\AllocationResource\Pages;
use App\Facades\Activity;
use App\Filament\Server\Resources\AllocationResource;
use App\Models\Allocation;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Allocations\FindAssignableAllocationService;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Facades\Filament;
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
{
protected static string $resource = AllocationResource::class;
public function table(Table $table): Table
{
/** @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 */
$server = Filament::getTenant();
return [
Actions\Action::make('addAllocation')
Action::make('addAllocation')
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
->hidden(fn () => !config('panel.client_features.allocations.enabled'))

View File

@ -2,16 +2,38 @@
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\Http\Controllers\Api\Client\Servers\BackupController;
use App\Models\Backup;
use App\Models\Permission;
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\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
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\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\Http\Request;
class BackupResource extends Resource
{
@ -51,8 +73,121 @@ class BackupResource extends Resource
return null;
}
return $count >= $limit ? 'danger'
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
}
public static 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 static 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),
]),
]);
}
public static function canViewAny(): bool

View File

@ -2,165 +2,28 @@
namespace App\Filament\Server\Resources\BackupResource\Pages;
use App\Enums\BackupStatus;
use App\Enums\ServerState;
use App\Facades\Activity;
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\Server;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Services\Backups\DownloadLinkService;
use App\Services\Backups\InitiateBackupService;
use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use Filament\Actions;
use Filament\Actions\CreateAction;
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\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;
class ListBackups extends ListRecords
{
protected static string $resource = BackupResource::class;
protected static bool $canCreateAnother = false;
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 */
$server = Filament::getTenant();
return [
Actions\CreateAction::make()
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)

View File

@ -2,16 +2,26 @@
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\Models\Database;
use App\Models\Permission;
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 Filament\Facades\Filament;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
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 Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class DatabaseResource extends Resource
{
@ -49,9 +59,65 @@ class DatabaseResource extends Resource
return null;
}
return $count >= $limit
? 'danger'
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
}
public static function form(Form $form): Form
{
/** @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 static 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)),
]);
}
public static function canViewAny(): bool

View File

@ -2,12 +2,8 @@
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\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Databases\DatabaseManagementService;
use Filament\Actions\CreateAction;
@ -15,76 +11,12 @@ use Filament\Facades\Filament;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
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
{
protected static string $resource = DatabaseResource::class;
public function form(Form $form): Form
{
/** @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 */

View File

@ -2,6 +2,8 @@
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\RelationManagers\TasksRelationManager;
use App\Helpers\Utilities;
@ -25,6 +27,12 @@ use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
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;
class ScheduleResource extends Resource
@ -296,6 +304,44 @@ class ScheduleResource extends Resource
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('cron')
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
TextColumn::make('status')
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
IconColumn::make('only_when_online')
->boolean()
->sortable(),
DateTimeColumn::make('last_run_at')
->label('Last run')
->placeholder('Never')
->since()
->sortable(),
DateTimeColumn::make('next_run_at')
->label('Next run')
->placeholder('Never')
->since()
->sortable()
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
])
->actions([
ViewAction::make(),
EditAction::make(),
DeleteAction::make()
->after(function (Schedule $schedule) {
Activity::event('server:schedule.delete')
->subject($schedule)
->property('name', $schedule->name)
->log();
}),
]);
}
public static function getDefaultRelations(): array
{
return [

View File

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

View File

@ -7,7 +7,8 @@ use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Permission;
use App\Models\Schedule;
use App\Services\Schedules\ProcessScheduleService;
use Filament\Actions;
use Filament\Actions\Action;
use Filament\Actions\EditAction;
use Filament\Facades\Filament;
use Filament\Resources\Pages\ViewRecord;
@ -18,7 +19,7 @@ class ViewSchedule extends ViewRecord
protected function getHeaderActions(): array
{
return [
Actions\Action::make('runNow')
Action::make('runNow')
->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'))
->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary')
@ -33,7 +34,7 @@ class ViewSchedule extends ViewRecord
$this->fillForm();
}),
Actions\EditAction::make(),
EditAction::make(),
];
}

View File

@ -30,7 +30,7 @@ class AccountCreated extends Notification implements ShouldQueue
->line('Email: ' . $notifiable->email);
if (!is_null($this->token)) {
return $message->action('Setup Your Account', Filament::getResetPasswordUrl($this->token, $notifiable));
return $message->action('Setup Your Account', Filament::getPanel('app')->getResetPasswordUrl($this->token, $notifiable));
}
return $message;

View File

@ -18,7 +18,7 @@
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.3",
"guzzlehttp/guzzle": "^7.9",
"laravel/framework": "^12.13",
"laravel/framework": "^12.15",
"laravel/helpers": "^1.7",
"laravel/sanctum": "^4.1",
"laravel/socialite": "^5.20",

198
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a24a7b46deafee826d9c132b6554e97b",
"content-hash": "0779ca7c1d1cbeeb09f80ec120490078",
"packages": [
{
"name": "abdelhamiderrahmouni/filament-monaco-editor",
@ -1020,16 +1020,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.343.6",
"version": "3.343.14",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "3746aca8cbed5f46beba850e0a480ef58e71b197"
"reference": "8bb5b542b28c4538b44de4335396e77bf9fbedf6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3746aca8cbed5f46beba850e0a480ef58e71b197",
"reference": "3746aca8cbed5f46beba850e0a480ef58e71b197",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8bb5b542b28c4538b44de4335396e77bf9fbedf6",
"reference": "8bb5b542b28c4538b44de4335396e77bf9fbedf6",
"shasum": ""
},
"require": {
@ -1111,9 +1111,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.343.6"
"source": "https://github.com/aws/aws-sdk-php/tree/3.343.14"
},
"time": "2025-05-07T18:10:08+00:00"
"time": "2025-05-19T18:02:45+00:00"
},
{
"name": "aymanalhattami/filament-context-menu",
@ -2630,16 +2630,16 @@
},
{
"name": "filament/actions",
"version": "v3.3.14",
"version": "v3.3.15",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/actions.git",
"reference": "08caa8dec43ebf4192dcd999cca786656a3dbc90"
"reference": "66b509aa72fa882ce91218eb743684a9350bc3fb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/08caa8dec43ebf4192dcd999cca786656a3dbc90",
"reference": "08caa8dec43ebf4192dcd999cca786656a3dbc90",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/66b509aa72fa882ce91218eb743684a9350bc3fb",
"reference": "66b509aa72fa882ce91218eb743684a9350bc3fb",
"shasum": ""
},
"require": {
@ -2679,20 +2679,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:43+00:00"
"time": "2025-05-19T07:25:24+00:00"
},
{
"name": "filament/filament",
"version": "v3.3.14",
"version": "v3.3.15",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/panels.git",
"reference": "2c4783bdd973967cc2dbc2dc518c70b04839ace3"
"reference": "bc83f6ca48340cc4127994687be121462fed9b7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/2c4783bdd973967cc2dbc2dc518c70b04839ace3",
"reference": "2c4783bdd973967cc2dbc2dc518c70b04839ace3",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/bc83f6ca48340cc4127994687be121462fed9b7a",
"reference": "bc83f6ca48340cc4127994687be121462fed9b7a",
"shasum": ""
},
"require": {
@ -2744,20 +2744,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:38+00:00"
"time": "2025-05-19T07:26:37+00:00"
},
{
"name": "filament/forms",
"version": "v3.3.14",
"version": "v3.3.15",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/forms.git",
"reference": "22e62dc2b4c68018e9846aadf7e8c5310d0e38cf"
"reference": "0e46d1d14e6f30a57dd85103d2b07aff52a75a5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/22e62dc2b4c68018e9846aadf7e8c5310d0e38cf",
"reference": "22e62dc2b4c68018e9846aadf7e8c5310d0e38cf",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/0e46d1d14e6f30a57dd85103d2b07aff52a75a5a",
"reference": "0e46d1d14e6f30a57dd85103d2b07aff52a75a5a",
"shasum": ""
},
"require": {
@ -2800,11 +2800,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:39+00:00"
"time": "2025-05-19T07:26:45+00:00"
},
{
"name": "filament/infolists",
"version": "v3.3.14",
"version": "v3.3.15",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/infolists.git",
@ -2855,7 +2855,7 @@
},
{
"name": "filament/notifications",
"version": "v3.3.14",
"version": "v3.3.15",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/notifications.git",
@ -2907,7 +2907,7 @@
},
{
"name": "filament/support",
"version": "v3.3.14",
"version": "v3.3.15",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/support.git",
@ -2966,16 +2966,16 @@
},
{
"name": "filament/tables",
"version": "v3.3.14",
"version": "v3.3.15",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/tables.git",
"reference": "bb5fad7306c39fdbb08d97982073114ac465bf92"
"reference": "64806e3c13caeabb23a8668a7aaf1efc8395df96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/bb5fad7306c39fdbb08d97982073114ac465bf92",
"reference": "bb5fad7306c39fdbb08d97982073114ac465bf92",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/64806e3c13caeabb23a8668a7aaf1efc8395df96",
"reference": "64806e3c13caeabb23a8668a7aaf1efc8395df96",
"shasum": ""
},
"require": {
@ -3014,11 +3014,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-04-30T09:16:33+00:00"
"time": "2025-05-19T07:26:42+00:00"
},
{
"name": "filament/widgets",
"version": "v3.3.14",
"version": "v3.3.15",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/widgets.git",
@ -3727,16 +3727,16 @@
},
{
"name": "kirschbaum-development/eloquent-power-joins",
"version": "4.2.3",
"version": "4.2.4",
"source": {
"type": "git",
"url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
"reference": "d04e06b12e5e7864c303b8a8c6045bfcd4e2c641"
"reference": "4a8012cef7abed8ac3633a180c69138a228b6c4c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/d04e06b12e5e7864c303b8a8c6045bfcd4e2c641",
"reference": "d04e06b12e5e7864c303b8a8c6045bfcd4e2c641",
"url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/4a8012cef7abed8ac3633a180c69138a228b6c4c",
"reference": "4a8012cef7abed8ac3633a180c69138a228b6c4c",
"shasum": ""
},
"require": {
@ -3784,22 +3784,22 @@
],
"support": {
"issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues",
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.3"
"source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.2.4"
},
"time": "2025-04-01T14:41:56+00:00"
"time": "2025-05-19T14:14:41+00:00"
},
{
"name": "laravel/framework",
"version": "v12.13.0",
"version": "v12.15.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "52b588bcd8efc6d01bc1493d2d67848f8065f269"
"reference": "2ef7fb183f18e547af4eb9f5a55b2ac1011f0b77"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/52b588bcd8efc6d01bc1493d2d67848f8065f269",
"reference": "52b588bcd8efc6d01bc1493d2d67848f8065f269",
"url": "https://api.github.com/repos/laravel/framework/zipball/2ef7fb183f18e547af4eb9f5a55b2ac1011f0b77",
"reference": "2ef7fb183f18e547af4eb9f5a55b2ac1011f0b77",
"shasum": ""
},
"require": {
@ -4001,7 +4001,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-05-07T17:29:01+00:00"
"time": "2025-05-20T15:10:44+00:00"
},
{
"name": "laravel/helpers",
@ -6160,31 +6160,31 @@
},
{
"name": "nunomaduro/termwind",
"version": "v2.3.0",
"version": "v2.3.1",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/termwind.git",
"reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda"
"reference": "dfa08f390e509967a15c22493dc0bac5733d9123"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/52915afe6a1044e8b9cee1bcff836fb63acf9cda",
"reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda",
"url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123",
"reference": "dfa08f390e509967a15c22493dc0bac5733d9123",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.2",
"symfony/console": "^7.1.8"
"symfony/console": "^7.2.6"
},
"require-dev": {
"illuminate/console": "^11.33.2",
"laravel/pint": "^1.18.2",
"illuminate/console": "^11.44.7",
"laravel/pint": "^1.22.0",
"mockery/mockery": "^1.6.12",
"pestphp/pest": "^2.36.0",
"phpstan/phpstan": "^1.12.11",
"phpstan/phpstan-strict-rules": "^1.6.1",
"symfony/var-dumper": "^7.1.8",
"pestphp/pest": "^2.36.0 || ^3.8.2",
"phpstan/phpstan": "^1.12.25",
"phpstan/phpstan-strict-rules": "^1.6.2",
"symfony/var-dumper": "^7.2.6",
"thecodingmachine/phpstan-strict-rules": "^1.0.0"
},
"type": "library",
@ -6227,7 +6227,7 @@
],
"support": {
"issues": "https://github.com/nunomaduro/termwind/issues",
"source": "https://github.com/nunomaduro/termwind/tree/v2.3.0"
"source": "https://github.com/nunomaduro/termwind/tree/v2.3.1"
},
"funding": [
{
@ -6243,7 +6243,7 @@
"type": "github"
}
],
"time": "2024-11-21T10:39:51+00:00"
"time": "2025-05-08T08:14:37+00:00"
},
{
"name": "openspout/openspout",
@ -8002,16 +8002,16 @@
},
{
"name": "secondnetwork/blade-tabler-icons",
"version": "v3.31.0",
"version": "v3.33.0",
"source": {
"type": "git",
"url": "https://github.com/secondnetwork/blade-tabler-icons.git",
"reference": "bf069bbdb2a63aa8eaeb10e6fd993464e9340a31"
"reference": "523b3ede493ce9f8cbe3a5bdce21769976a80b28"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/bf069bbdb2a63aa8eaeb10e6fd993464e9340a31",
"reference": "bf069bbdb2a63aa8eaeb10e6fd993464e9340a31",
"url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/523b3ede493ce9f8cbe3a5bdce21769976a80b28",
"reference": "523b3ede493ce9f8cbe3a5bdce21769976a80b28",
"shasum": ""
},
"require": {
@ -8054,9 +8054,9 @@
],
"support": {
"issues": "https://github.com/secondnetwork/blade-tabler-icons/issues",
"source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.31.0"
"source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.33.0"
},
"time": "2025-03-06T09:24:43+00:00"
"time": "2025-05-17T06:28:48+00:00"
},
{
"name": "socialiteproviders/authentik",
@ -8705,16 +8705,16 @@
},
{
"name": "spatie/laravel-health",
"version": "1.34.1",
"version": "1.34.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-health.git",
"reference": "283d7d5dffeeff6452c9a182f466c327fec6db3b"
"reference": "e7d9d6e0807a9de46f544c76d54badc19af36ddc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-health/zipball/283d7d5dffeeff6452c9a182f466c327fec6db3b",
"reference": "283d7d5dffeeff6452c9a182f466c327fec6db3b",
"url": "https://api.github.com/repos/spatie/laravel-health/zipball/e7d9d6e0807a9de46f544c76d54badc19af36ddc",
"reference": "e7d9d6e0807a9de46f544c76d54badc19af36ddc",
"shasum": ""
},
"require": {
@ -8786,7 +8786,7 @@
"spatie"
],
"support": {
"source": "https://github.com/spatie/laravel-health/tree/1.34.1"
"source": "https://github.com/spatie/laravel-health/tree/1.34.2"
},
"funding": [
{
@ -8794,7 +8794,7 @@
"type": "github"
}
],
"time": "2025-04-17T06:34:01+00:00"
"time": "2025-05-20T08:31:02+00:00"
},
{
"name": "spatie/laravel-package-tools",
@ -8859,16 +8859,16 @@
},
{
"name": "spatie/laravel-permission",
"version": "6.17.0",
"version": "6.18.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-permission.git",
"reference": "02ada8f638b643713fa2fb543384738e27346ddb"
"reference": "3c05f04d12275dfbe462c8b4aae3290e586c2dde"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/02ada8f638b643713fa2fb543384738e27346ddb",
"reference": "02ada8f638b643713fa2fb543384738e27346ddb",
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/3c05f04d12275dfbe462c8b4aae3290e586c2dde",
"reference": "3c05f04d12275dfbe462c8b4aae3290e586c2dde",
"shasum": ""
},
"require": {
@ -8930,7 +8930,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-permission/issues",
"source": "https://github.com/spatie/laravel-permission/tree/6.17.0"
"source": "https://github.com/spatie/laravel-permission/tree/6.18.0"
},
"funding": [
{
@ -8938,7 +8938,7 @@
"type": "github"
}
],
"time": "2025-04-08T15:06:14+00:00"
"time": "2025-05-14T03:32:23+00:00"
},
{
"name": "spatie/laravel-query-builder",
@ -13132,16 +13132,16 @@
},
{
"name": "laravel/pint",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36"
"reference": "941d1927c5ca420c22710e98420287169c7bcaf7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
"reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
"url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7",
"reference": "941d1927c5ca420c22710e98420287169c7bcaf7",
"shasum": ""
},
"require": {
@ -13153,11 +13153,11 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.75.0",
"illuminate/view": "^11.44.2",
"larastan/larastan": "^3.3.1",
"illuminate/view": "^11.44.7",
"larastan/larastan": "^3.4.0",
"laravel-zero/framework": "^11.36.1",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3",
"nunomaduro/termwind": "^2.3.1",
"pestphp/pest": "^2.36.0"
},
"bin": [
@ -13194,20 +13194,20 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-04-08T22:11:45+00:00"
"time": "2025-05-08T08:38:12+00:00"
},
{
"name": "laravel/sail",
"version": "v1.42.0",
"version": "v1.43.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
"reference": "2edaaf77f3c07a4099965bb3d7dfee16e801c0f6"
"reference": "71a509b14b2621ce58574274a74290f933c687f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/2edaaf77f3c07a4099965bb3d7dfee16e801c0f6",
"reference": "2edaaf77f3c07a4099965bb3d7dfee16e801c0f6",
"url": "https://api.github.com/repos/laravel/sail/zipball/71a509b14b2621ce58574274a74290f933c687f7",
"reference": "71a509b14b2621ce58574274a74290f933c687f7",
"shasum": ""
},
"require": {
@ -13257,7 +13257,7 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
"time": "2025-04-29T14:26:46+00:00"
"time": "2025-05-13T13:34:34+00:00"
},
{
"name": "mockery/mockery",
@ -14016,16 +14016,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.14",
"version": "2.1.16",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2"
"reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2",
"reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9",
"reference": "b8c1cf533cba0c305d91c6ccd23f3dd0566ba5f9",
"shasum": ""
},
"require": {
@ -14070,7 +14070,7 @@
"type": "github"
}
],
"time": "2025-05-02T15:32:28+00:00"
"time": "2025-05-16T09:40:10+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -15424,16 +15424,16 @@
},
{
"name": "spatie/backtrace",
"version": "1.7.3",
"version": "1.7.4",
"source": {
"type": "git",
"url": "https://github.com/spatie/backtrace.git",
"reference": "80ae5fabe2a1e3bc7df5c7ca5d91b094dc314b20"
"reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/backtrace/zipball/80ae5fabe2a1e3bc7df5c7ca5d91b094dc314b20",
"reference": "80ae5fabe2a1e3bc7df5c7ca5d91b094dc314b20",
"url": "https://api.github.com/repos/spatie/backtrace/zipball/cd37a49fce7137359ac30ecc44ef3e16404cccbe",
"reference": "cd37a49fce7137359ac30ecc44ef3e16404cccbe",
"shasum": ""
},
"require": {
@ -15471,7 +15471,7 @@
"spatie"
],
"support": {
"source": "https://github.com/spatie/backtrace/tree/1.7.3"
"source": "https://github.com/spatie/backtrace/tree/1.7.4"
},
"funding": [
{
@ -15483,7 +15483,7 @@
"type": "other"
}
],
"time": "2025-05-07T07:20:00+00:00"
"time": "2025-05-08T15:41:09+00:00"
},
{
"name": "spatie/error-solutions",
@ -15966,7 +15966,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@ -15977,7 +15977,7 @@
"ext-pdo": "*",
"ext-zip": "*"
},
"platform-dev": {},
"platform-dev": [],
"platform-overrides": {
"php": "8.2"
},