This commit is contained in:
Saeed Vaziry
2024-09-27 20:36:03 +02:00
committed by GitHub
parent b62c40c97d
commit f6bc04763b
122 changed files with 6609 additions and 807 deletions

View File

@ -0,0 +1,72 @@
<?php
namespace App\Web\Fields;
use Filament\Forms\Components\Field;
class AlertField extends Field
{
protected string $view = 'web.fields.alert';
public string $color = 'blue';
public string $icon = 'heroicon-o-information-circle';
public string $message = '';
public function color(string $color): static
{
$this->color = $color;
return $this;
}
public function icon(string $icon): static
{
$this->icon = $icon;
return $this;
}
public function message(string $message): static
{
$this->message = $message;
return $this;
}
public function success(): static
{
return $this->color('green')->icon('heroicon-o-check-circle');
}
public function warning(): static
{
return $this->color('yellow')->icon('heroicon-o-exclamation-circle');
}
public function danger(): static
{
return $this->color('red')->icon('heroicon-o-x-circle');
}
public function info(): static
{
return $this->color('blue')->icon('heroicon-o-information-circle');
}
public function getColor(): string
{
return $this->color;
}
public function getIcon(): string
{
return $this->icon;
}
public function getMessage(): string
{
return $this->message;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Web\Fields;
use Filament\Forms\Components\Field;
class ProviderField extends Field
{
protected string $view = 'web.fields.provider';
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Web\Pages;
use Filament\Widgets\AccountWidget;
class Dashboard extends \Filament\Pages\Dashboard
{
public function getWidgets(): array
{
return [
AccountWidget::class,
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Web\Pages\Servers;
use App\Models\Server;
use App\Web\Traits\PageHasWidgets;
use Filament\Actions\Action;
use Filament\Pages\Page;
class Create extends Page
{
use PageHasWidgets;
protected static ?string $slug = 'servers/create';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Create Server';
public static function canAccess(): bool
{
return auth()->user()?->can('create', Server::class) ?? false;
}
protected function getExtraAttributes(): array
{
return [];
}
public function getWidgets(): array
{
return [
[Widgets\CreateServer::class],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/servers/create-server.html')
->openUrlInNewTab(),
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Web\Pages\Servers;
use App\Models\Server;
use App\Web\Traits\PageHasWidgets;
use Filament\Actions\Action;
use Filament\Pages\Page;
class Index extends Page
{
use PageHasWidgets;
protected static ?string $slug = 'servers';
protected static ?string $navigationIcon = 'heroicon-o-server-stack';
protected static ?int $navigationSort = 1;
protected static ?string $title = 'Servers';
public static function getNavigationItemActiveRoutePattern(): string
{
return static::getRouteName().'*';
}
public function getWidgets(): array
{
return [
[Widgets\ServersList::class],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('create')
->label('Create a Server')
->url(Create::getUrl())
->authorize('create', Server::class),
];
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace App\Web\Pages\Servers;
use App\Actions\Server\RebootServer;
use App\Models\Server;
use App\Web\Pages\Servers\Widgets\ServerDetails;
use App\Web\Pages\Servers\Widgets\UpdateServerInfo;
use App\Web\Traits\PageHasServer;
use App\Web\Traits\PageHasWidgets;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
class Settings extends Page
{
use PageHasServer;
use PageHasWidgets;
protected static ?string $slug = 'servers/{server}/settings';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Settings';
protected $listeners = ['$refresh'];
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('update', request()->route('server')) ?? false;
}
public function getWidgets(): array
{
return [
[
ServerDetails::class, ['server' => $this->server],
],
[
UpdateServerInfo::class, ['server' => $this->server],
],
];
}
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
->icon('heroicon-o-trash')
->record($this->server)
->modalHeading('Delete Server')
->modalDescription('Once your server is deleted, all of its resources and data will be permanently deleted and can\'t be restored'),
Action::make('reboot')
->color('gray')
->icon('heroicon-o-arrow-path')
->label('Reboot')
->requiresConfirmation()
->action(function () {
app(RebootServer::class)->reboot($this->server);
$this->dispatch('$refresh');
Notification::make()
->info()
->title('Server is being rebooted')
->send();
}),
];
}
protected function getServer(): ?Server
{
return $this->server;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Web\Pages\Servers\Sites;
use App\Models\Server;
use App\Models\Site;
use App\Web\Pages\Servers\Sites\Widgets\SitesList;
use App\Web\Traits\PageHasServer;
use App\Web\Traits\PageHasWidgets;
use Filament\Actions\CreateAction;
use Filament\Pages\Page;
class Index extends Page
{
use PageHasServer;
use PageHasWidgets;
protected static ?string $slug = 'servers/{server}/sites';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Sites';
public Server $server;
public function getWidgets(): array
{
return [
[SitesList::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->authorize(fn () => auth()->user()?->can('create', [Site::class, $this->server]))
->createAnother(false)
->label('Create a Site'),
];
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Web\Pages\Servers\Sites\Widgets;
use App\Models\Server;
use App\Models\Site;
use App\Web\Pages\Servers\View;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class SitesList extends Widget
{
public Server $server;
protected function getTableQuery(): Builder
{
return Site::query()->where('server_id', $this->server->id);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('id')
->searchable()
->sortable(),
TextColumn::make('server.name')
->searchable()
->sortable(),
TextColumn::make('domain')
->searchable(),
TextColumn::make('status')
->label('Status')
->badge()
->color(fn (Site $site) => Site::$statusColors[$site->status])
->searchable()
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
// ->recordUrl(fn (Server $record) => View::getUrl(parameters: ['server' => $record]))
->actions([
// Action::make('settings')
// ->label('Settings')
// ->icon('heroicon-o-cog-6-tooth')
// ->authorize(fn ($record) => auth()->user()->can('update', $record))
// ->url(fn (Server $record) => '/'),
]);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Web\Pages\Servers;
use App\Models\Server;
use App\Web\Pages\Servers\Widgets\Installing;
use App\Web\Traits\PageHasServer;
use App\Web\Traits\PageHasWidgets;
use Filament\Pages\Page;
use Livewire\Attributes\On;
class View extends Page
{
use PageHasServer;
use PageHasWidgets;
protected static ?string $slug = 'servers/{server}';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Overview';
public Server $server;
public string $previousStatus;
public function mount(): void
{
$this->previousStatus = $this->server->status;
}
public static function canAccess(): bool
{
return auth()->user()?->can('view', request()->route('server')) ?? false;
}
#[On('$refresh')]
public function refresh(): void
{
$currentStatus = $this->server->refresh()->status;
if ($this->previousStatus !== $currentStatus) {
$this->redirect(static::getUrl(parameters: ['server' => $this->server]));
}
$this->previousStatus = $currentStatus;
}
public function getWidgets(): array
{
if ($this->server->isInstalling()) {
return [
[Installing::class, ['server' => $this->server]],
];
}
return [];
}
}

View File

@ -0,0 +1,263 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Actions\Server\CreateServer as CreateServerAction;
use App\Enums\ServerProvider;
use App\Enums\ServerType;
use App\Enums\Webserver;
use App\Models\Server;
use App\Web\Fields\AlertField;
use App\Web\Fields\ProviderField;
use App\Web\Pages\Servers\Index;
use App\Web\Pages\Settings\ServerProviders\Actions\Create;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Filament\Widgets\Widget;
use Throwable;
class CreateServer extends Widget implements HasForms
{
use InteractsWithForms;
protected static string $view = 'web.components.form';
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
public ?string $provider = ServerProvider::HETZNER;
public ?string $server_provider = '';
public ?string $region = '';
public ?string $plan = '';
public ?string $public_key = '';
public ?string $name = '';
public ?string $ip = '';
public ?string $port = '';
public ?string $os = '';
public ?string $type = ServerType::REGULAR;
public ?string $webserver = Webserver::NGINX;
public ?string $database = '';
public ?string $php = '';
public function form(Form $form): Form
{
$publicKey = __('servers.create.public_key_text', [
'public_key' => get_public_key_content(),
]);
return $form
->schema([
ProviderField::make('provider')
->label('Select a provider')
->default(ServerProvider::CUSTOM)
->live()
->reactive()
->reactive()
->afterStateUpdated(function (callable $set) {
$set('server_provider', null);
$set('region', null);
$set('plan', null);
})
->rules(fn ($get) => CreateServerAction::rules($this->all())['provider']),
AlertField::make('alert')
->warning()
->message(__('servers.create.public_key_warning'))
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
Select::make('server_provider')
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM)
->label('Server provider connection')
->rules(fn ($get) => CreateServerAction::rules($this->all())['server_provider'])
->options(function ($get) {
return \App\Models\ServerProvider::getByProjectId(auth()->user()->current_project_id)
->where('provider', $this->provider)
->pluck('profile', 'id');
})
->live()
->suffixAction(
Action::make('connect')
->form(Create::form())
->modalHeading('Connect to a new server provider')
->modalSubmitActionLabel('Connect')
->icon('heroicon-o-wifi')
->tooltip('Connect to a new server provider')
->modalWidth(MaxWidth::Medium)
->authorize(fn () => auth()->user()->can('create', \App\Models\ServerProvider::class))
// TODO: remove this after filament #14319 is fixed
->url(\App\Web\Pages\Settings\ServerProviders\Index::getUrl())
->action(fn (array $data) => Create::action($data))
)
->placeholder('Select profile')
->native(false)
->selectablePlaceholder(false)
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM),
Grid::make()
->schema([
Select::make('region')
->label('Region')
->rules(fn ($get) => CreateServerAction::rules($this->all())['region'])
->live()
->reactive()
->options(function () {
if (! $this->server_provider) {
return [];
}
return \App\Models\ServerProvider::regions($this->server_provider);
})
->loadingMessage('Loading regions...')
->disabled(fn ($get) => ! $this->server_provider)
->placeholder(fn ($get) => $this->server_provider ? 'Select region' : 'Select connection first')
->searchable(),
Select::make('plan')
->label('Plan')
->rules(fn ($get) => CreateServerAction::rules($this->all())['plan'])
->reactive()
->options(function () {
if (! $this->server_provider || ! $this->region) {
return [];
}
return \App\Models\ServerProvider::plans($this->server_provider, $this->region);
})
->loadingMessage('Loading plans...')
->disabled(fn ($get) => ! $this->region)
->placeholder(fn ($get) => $this->region ? 'Select plan' : 'Select plan first')
->searchable(),
])
->visible(fn ($get) => $this->provider !== ServerProvider::CUSTOM),
TextInput::make('public_key')
->label('Public Key')
->default($publicKey)
->suffixAction(
Action::make('copy')
->icon('heroicon-o-clipboard-document-list')
->tooltip('Copy')
->action(function ($livewire, $state) {
$livewire->js(
'window.navigator.clipboard.writeText("'.$state.'");'
);
Notification::make()
->success()
->title('Copied!')
->send();
})
)
->helperText('Run this command on your server as root user')
->disabled()
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
TextInput::make('name')
->label('Name')
->rules(fn ($get) => CreateServerAction::rules($this->all())['name']),
Grid::make()
->schema([
TextInput::make('ip')
->label('SSH IP Address')
->rules(fn ($get) => CreateServerAction::rules($this->all())['ip']),
TextInput::make('port')
->label('SSH Port')
->rules(fn ($get) => CreateServerAction::rules($this->all())['port']),
])
->visible(fn ($get) => $this->provider === ServerProvider::CUSTOM),
Grid::make()
->schema([
Select::make('os')
->label('OS')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['os'])
->options(function () {
return collect(config('core.operating_systems'))
->mapWithKeys(fn ($value) => [$value => $value]);
}),
Select::make('type')
->label('Server Type')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['type'])
->options(function () {
return collect(config('core.server_types'))
->mapWithKeys(fn ($value) => [$value => $value]);
})
->default(ServerType::REGULAR),
]),
Grid::make(3)
->schema([
Select::make('webserver')
->label('Webserver')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['webserver'] ?? [])
->options(function () {
return collect(config('core.webservers'))
->mapWithKeys(fn ($value) => [$value => $value]);
}),
Select::make('database')
->label('Database')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['database'] ?? [])
->options(function () {
return collect(config('core.databases_name'))
->mapWithKeys(fn ($value, $key) => [
$key => $value.' '.config('core.databases_version')[$key],
]);
}),
Select::make('php')
->label('PHP')
->native(false)
->selectablePlaceholder(false)
->rules(fn ($get) => CreateServerAction::rules($this->all())['php'] ?? [])
->options(function () {
return collect(config('core.php_versions'))
->mapWithKeys(fn ($value) => [$value => $value]);
}),
]),
Actions::make([
Action::make('create')
->label('Create Server')
->button()
->action(fn () => $this->submit()),
]),
])
->columns(1);
}
public function submit(): void
{
$this->authorize('create', Server::class);
$this->validate();
try {
$server = app(CreateServerAction::class)->create(auth()->user(), $this->all()['data']);
$this->redirect(Index::getUrl());
} catch (Throwable $e) {
Notification::make()
->title($e->getMessage())
->danger()
->send();
}
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Models\Server;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Components\ViewEntry;
use Filament\Infolists\Concerns\InteractsWithInfolists;
use Filament\Infolists\Contracts\HasInfolists;
use Filament\Infolists\Infolist;
use Filament\Widgets\Widget;
use Illuminate\View\ComponentAttributeBag;
class Installing extends Widget implements HasForms, HasInfolists
{
use InteractsWithForms;
use InteractsWithInfolists;
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
protected static string $view = 'web.components.infolist';
public Server $server;
public function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make()
->heading('Installing Server')
->icon(function () {
if ($this->server->isInstallationFailed()) {
return 'heroicon-o-x-circle';
}
return view('filament::components.loading-indicator')
->with('attributes', new ComponentAttributeBag([
'class' => 'mr-2 size-[24px] text-primary-400',
]));
})
->iconColor($this->server->isInstallationFailed() ? 'danger' : 'primary')
->schema([
ViewEntry::make('progress')
->hiddenLabel()
->view('components.progress-bar')
->viewData([
'value' => $this->server->progress,
]),
]),
])
->record($this->server->refresh());
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Models\Server;
use App\Web\Resources\Server\Pages\CreateServer;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Widgets\Widget;
use Illuminate\Database\Eloquent\Builder;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class SelectServer extends Widget implements HasForms
{
use InteractsWithForms;
protected static string $view = 'web.components.form';
public int|string|null $server = null;
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function mount(): void
{
$server = Server::query()
->where('project_id', auth()->user()->current_project_id)
->find(session()->get('current_server_id'));
if ($server && auth()->user()->can('view', $server)) {
$this->server = $server->id;
}
}
protected function getFormSchema(): array
{
$options = $this->query()
->limit(10)
->pluck('name', 'id')
->toArray();
return [
Select::make('server')
->name('server')
->model($this->server)
->searchable()
->options($options)
->searchPrompt('Search...')
->getSearchResultsUsing(function ($search) {
return $this->query()
->where('name', 'like', "%{$search}%")
->limit(10)
->pluck('name', 'id')
->toArray();
})
->extraAttributes(['class' => '-mx-2 pointer-choices'])
->live()
->hintIcon('heroicon-o-question-mark-circle')
->hintIconTooltip('Filter resources by default based on a server')
->suffixAction(
Action::make('create')
->icon('heroicon-o-plus')
->tooltip('Create a new server')
->url(CreateServer::getUrl())
),
];
}
private function query(): Builder
{
return Server::query()
->where(function (Builder $query) {
$query->where('project_id', auth()->user()->current_project_id);
});
}
public function updatedServer($value): void
{
if (! $value) {
session()->forget('current_server_id');
$this->redirect(url()->previous());
return;
}
$server = Server::query()->find($value);
if (! $server) {
session()->forget('current_server_id');
return;
}
$this->authorize('view', $server);
session()->put('current_server_id', $value);
$this->redirect(url()->previous());
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Actions\Server\Update;
use App\Models\Server;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Components\Actions\Action;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Concerns\InteractsWithInfolists;
use Filament\Infolists\Contracts\HasInfolists;
use Filament\Infolists\Infolist;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
class ServerDetails extends Widget implements HasForms, HasInfolists
{
use InteractsWithForms;
use InteractsWithInfolists;
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
protected static string $view = 'web.components.infolist';
public Server $server;
public function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make()
->heading('Server Details')
->description('More details about your server')
->columns(1)
->schema([
TextEntry::make('id')
->label('ID')
->inlineLabel()
->hintIcon('heroicon-o-information-circle')
->hintIconTooltip('Server unique identifier to use in the API'),
TextEntry::make('created_at_by_timezone')
->label('Created At')
->inlineLabel(),
TextEntry::make('last_updated_check')
->label('Last Updated Check')
->inlineLabel()
->state(fn (Server $record) => $record->getDateTimeByTimezone($record->last_update_check) ?? '-')
->suffixAction(
Action::make('check-update')
->icon('heroicon-o-arrow-path')
->tooltip('Check Now')
->action(fn (Server $record) => $record->checkForUpdates())
),
TextEntry::make('available_updates')
->label('Available Updates')
->inlineLabel()
->suffixAction(
Action::make('update-server')
->icon('heroicon-o-check-circle')
->tooltip('Update Now')
->action(function (Server $record) {
app(Update::class)->update($record);
$this->dispatch('$refresh');
Notification::make()
->info()
->title('Updating the server...')
->send();
})
),
TextEntry::make('provider')
->label('Provider')
->inlineLabel(),
TextEntry::make('tags')
->label('Tags')
->inlineLabel()
->state(fn (Server $record) => view('web.components.tags', ['tags' => $record->tags]))
->suffixAction(
Action::make('edit-tags')
->icon('heroicon-o-pencil')
->tooltip('Edit Tags')
->action(fn (Server $record) => $this->dispatch('$editTags', $record))
->tooltip('Edit Tags')
),
]),
])
->record($this->server);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Models\Server;
use App\Web\Pages\Servers\View;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Components\Actions\Action;
use Filament\Infolists\Components\Fieldset;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Concerns\InteractsWithInfolists;
use Filament\Infolists\Contracts\HasInfolists;
use Filament\Infolists\Infolist;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconPosition;
use Filament\Widgets\Widget;
class ServerSummary extends Widget implements HasForms, HasInfolists
{
use InteractsWithForms;
use InteractsWithInfolists;
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
protected static string $view = 'web.components.infolist';
public Server $server;
public function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Fieldset::make('info')
->label('Server Summary')
->schema([
TextEntry::make('name')
->label('Name')
->url(fn (Server $record) => View::getUrl(parameters: ['server' => $record])),
TextEntry::make('ip')
->label('IP Address')
->icon('heroicon-o-clipboard-document')
->iconPosition(IconPosition::After)
->copyable(),
TextEntry::make('status')
->label('Status')
->badge()
->color(static function ($state): string {
return Server::$statusColors[$state];
})
->suffixAction(
Action::make('check-status')
->icon('heroicon-o-arrow-path')
->tooltip('Check Connection')
->action(function (Server $record) {
$record = $record->checkConnection();
$this->dispatch('$refresh');
Notification::make()
->status(Server::$statusColors[$record->status])
->title('Server is '.$record->status)
->send();
})
),
])
->columns(3),
])
->record($this->server->refresh());
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Models\Server;
use App\Web\Pages\Servers\View;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ViewColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class ServersList extends Widget
{
protected function getTableQuery(): Builder
{
return Server::query()->where('project_id', auth()->user()->current_project_id);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('id')
->searchable()
->sortable(),
TextColumn::make('name')
->searchable()
->sortable(),
ViewColumn::make('tags.name')
->label('Tags')
->view('web.components.tags')
->extraCellAttributes(['class' => 'px-3'])
->searchable()
->sortable(),
TextColumn::make('status')
->label('Status')
->badge()
->color(fn (Server $server) => Server::$statusColors[$server->status])
->searchable()
->sortable(),
TextColumn::make('created_at_by_timezone')
->label('Created At')
->searchable()
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
->recordUrl(fn (Server $record) => View::getUrl(parameters: ['server' => $record]))
->actions([
Action::make('settings')
->label('Settings')
->icon('heroicon-o-cog-6-tooth')
->authorize(fn ($record) => auth()->user()->can('update', $record))
->url(fn (Server $record) => '/'),
]);
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Actions\Server\EditServer;
use App\Models\Server;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
class UpdateServerInfo extends Widget implements HasForms
{
use InteractsWithForms;
protected static bool $isLazy = false;
protected static string $view = 'web.components.form';
public Server $server;
public string $name;
public string $ip;
public string $port;
public function mount(Server $server): void
{
$this->server = $server;
$this->name = $server->name;
$this->ip = $server->ip;
$this->port = $server->port;
}
public function form(Form $form): Form
{
return $form
->schema([
Section::make()
->heading('Update Server')
->description('You can edit your server\'s information here')
->columns(1)
->schema([
TextInput::make('name')
->label('Name')
->rules(EditServer::rules($this->server)['name']),
TextInput::make('ip')
->label('IP Address')
->rules(EditServer::rules($this->server)['ip']),
TextInput::make('port')
->label('Port')
->rules(EditServer::rules($this->server)['port']),
])
->footerActions([
Actions\Action::make('submit')
->label('Save')
->action(fn () => $this->submit()),
]),
]);
}
public function submit(): void
{
$this->authorize('update', $this->server);
$this->validate();
app(EditServer::class)->edit($this->server, $this->all());
$this->dispatch('$refresh');
Notification::make()
->success()
->title('Server updated!')
->send();
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Web\Pages\Settings\Profile;
use App\Web\Pages\Settings\Profile\Widgets\ProfileInformation;
use App\Web\Pages\Settings\Profile\Widgets\TwoFactor;
use App\Web\Pages\Settings\Profile\Widgets\UpdatePassword;
use App\Web\Traits\PageHasWidgets;
use Filament\Pages\Page;
class Index extends Page
{
use PageHasWidgets;
protected static ?string $navigationGroup = 'Settings';
protected static ?string $slug = 'settings/profile';
protected static ?string $title = 'Profile';
protected static ?string $navigationIcon = 'heroicon-o-user-circle';
protected static ?int $navigationSort = 2;
public function getWidgets(): array
{
return [
[ProfileInformation::class],
[UpdatePassword::class],
[TwoFactor::class],
];
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Web\Pages\Settings\Profile\Widgets;
use App\Actions\User\UpdateUserProfileInformation;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
class ProfileInformation extends Widget implements HasForms
{
use InteractsWithForms;
protected static bool $isLazy = false;
protected static string $view = 'web.components.form';
public string $name;
public string $email;
public string $timezone;
public function mount(): void
{
$this->name = auth()->user()->name;
$this->email = auth()->user()->email;
$this->timezone = auth()->user()->timezone;
}
public function getFormSchema(): array
{
$rules = UpdateUserProfileInformation::rules(auth()->user());
return [
Section::make()
->heading('Profile Information')
->description('Update your account\'s profile information and email address.')
->schema([
TextInput::make('name')
->label('Name')
->rules($rules['name']),
TextInput::make('email')
->label('Email')
->rules($rules['email']),
Select::make('timezone')
->label('Timezone')
->searchable()
->options(
collect(timezone_identifiers_list())
->mapWithKeys(fn ($timezone) => [$timezone => $timezone])
)
->rules($rules['timezone']),
])
->footerActions([
Action::make('save')
->label('Save')
->action(fn () => $this->submit()),
]),
];
}
public function submit(): void
{
$this->validate();
app(UpdateUserProfileInformation::class)->update(auth()->user(), $this->all());
Notification::make()
->success()
->title('Profile updated!')
->send();
}
}

View File

@ -0,0 +1,138 @@
<?php
namespace App\Web\Pages\Settings\Profile\Widgets;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Components\Actions\Action;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Components\ViewEntry;
use Filament\Infolists\Concerns\InteractsWithInfolists;
use Filament\Infolists\Contracts\HasInfolists;
use Filament\Infolists\Infolist;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
class TwoFactor extends Widget implements HasForms, HasInfolists
{
use InteractsWithForms;
use InteractsWithInfolists;
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
protected static string $view = 'web.components.infolist';
public bool $enabled = false;
public bool $showCodes = false;
public function mount(): void
{
if (auth()->user()->two_factor_secret) {
$this->enabled = true;
}
}
public function infolist(Infolist $infolist): Infolist
{
return $infolist->schema([
Section::make()
->heading('Two Factor Authentication')
->description('Here you can activate 2FA to secure your account')
->schema([
TextEntry::make('disabled')
->hiddenLabel()
->state('Two factor authentication is disabled.')
->visible(! $this->enabled),
ViewEntry::make('qr_code')
->hiddenLabel()
->view('web.components.container', [
'content' => $this->enabled ? auth()->user()->twoFactorQrCodeSvg() : null,
])
->visible($this->enabled && $this->showCodes),
TextEntry::make('qr_code_manual')
->label('If you are unable to scan the QR code, please use the 2FA secret instead.')
->state($this->enabled ? decrypt(auth()->user()->two_factor_secret) : null)
->copyable()
->visible($this->enabled && $this->showCodes),
TextEntry::make('recovery_codes_text')
->hiddenLabel()
->color('warning')
->state('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.')
->visible($this->enabled),
ViewEntry::make('recovery_codes')
->hiddenLabel()
->extraAttributes(['class' => 'rounded-lg border border-gray-100 p-2 dark:border-gray-700'])
->view('web.components.container', [
'content' => $this->enabled ? implode('</br>', json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true)) : null,
])
->visible($this->enabled),
])
->footerActions([
Action::make('two-factor')
->color($this->enabled ? 'danger' : 'primary')
->label($this->enabled ? 'Disable' : 'Enable')
->action(function () {
if ($this->enabled) {
$this->disableTwoFactor();
} else {
$this->enableTwoFactor();
}
}),
Action::make('regenerate')
->color('gray')
->label('Regenerate Recovery Codes')
->visible($this->enabled)
->action(fn () => $this->regenerateRecoveryCodes()),
]),
]);
}
public function enableTwoFactor(): void
{
app(EnableTwoFactorAuthentication::class)(auth()->user());
$this->enabled = true;
$this->showCodes = true;
Notification::make()
->success()
->title('Two factor authentication enabled')
->send();
$this->dispatch('$refresh');
}
public function disableTwoFactor(): void
{
app(DisableTwoFactorAuthentication::class)(auth()->user());
$this->enabled = false;
$this->showCodes = false;
Notification::make()
->success()
->title('Two factor authentication disabled')
->send();
$this->dispatch('$refresh');
}
public function regenerateRecoveryCodes(): void
{
app(GenerateNewRecoveryCodes::class)(auth()->user());
Notification::make()
->success()
->title('Recovery codes generated')
->send();
$this->dispatch('$refresh');
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Web\Pages\Settings\Profile\Widgets;
use App\Actions\User\UpdateUserPassword;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
class UpdatePassword extends Widget implements HasForms
{
use InteractsWithForms;
protected static bool $isLazy = false;
protected static string $view = 'web.components.form';
public string $current_password = '';
public string $password = '';
public string $password_confirmation = '';
public function getFormSchema(): array
{
$rules = UpdateUserPassword::rules();
return [
Section::make()
->heading('Update Password')
->description('Ensure your account is using a long, random password to stay secure.')
->schema([
TextInput::make('current_password')
->label('Current Password')
->password()
->rules($rules['current_password']),
TextInput::make('password')
->label('New Password')
->password()
->rules($rules['password']),
TextInput::make('password_confirmation')
->label('Confirm Password')
->password()
->rules($rules['password_confirmation']),
])
->footerActions([
Action::make('save')
->label('Save')
->action(fn () => $this->submit()),
]),
];
}
public function submit(): void
{
$this->validate();
app(UpdateUserPassword::class)->update(auth()->user(), $this->all());
$this->current_password = '';
$this->password = '';
$this->password_confirmation = '';
Notification::make()
->success()
->title('Password updated!')
->send();
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Web\Pages\Settings\Projects;
use App\Actions\Projects\CreateProject;
use App\Models\Project;
use App\Web\Traits\PageHasWidgets;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Page;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
use PageHasWidgets;
protected static ?string $navigationGroup = 'Settings';
protected static ?string $slug = 'settings/projects';
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?int $navigationSort = 4;
protected static ?string $title = 'Projects';
public static function getNavigationItemActiveRoutePattern(): string
{
return static::getRouteName().'*';
}
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', Project::class) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\ProjectsList::class],
];
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Create Project')
->icon('heroicon-o-plus')
->authorize('create', Project::class)
->using(function (array $data) {
return app(CreateProject::class)->create(auth()->user(), $data);
})
->form(function (Form $form) {
return $form->schema([
TextInput::make('name')
->name('name')
->rules(CreateProject::rules()['name']),
])->columns(1);
})
->createAnother(false)
->modalWidth(MaxWidth::Large),
];
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Web\Pages\Settings\Projects;
use App\Actions\Projects\DeleteProject;
use App\Models\Project;
use App\Web\Pages\Settings\Projects\Widgets\AddUser;
use App\Web\Pages\Settings\Projects\Widgets\ProjectUsersList;
use App\Web\Pages\Settings\Projects\Widgets\UpdateProject;
use App\Web\Traits\PageHasWidgets;
use Exception;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Contracts\Support\Htmlable;
class Settings extends Page
{
use PageHasWidgets;
protected static ?string $slug = 'settings/projects/{project}';
protected static ?string $title = 'Project Settings';
protected static bool $shouldRegisterNavigation = false;
public static function canAccess(): bool
{
return auth()->user()?->can('update', request()->route('project')) ?? false;
}
public Project $project;
public function getWidgets(): array
{
return [
[
UpdateProject::class,
['project' => $this->project],
],
[
AddUser::class,
['project' => $this->project],
],
[
ProjectUsersList::class,
['project' => $this->project],
],
];
}
public function getTitle(): string|Htmlable
{
return 'Project Settings';
}
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
->record($this->project)
->label('Delete Project')
->modalHeading('Delete Project')
->modalDescription('Are you sure you want to delete this project? This action will delete all associated data and cannot be undone.')
->using(function (Project $record) {
try {
app(DeleteProject::class)->delete(auth()->user(), $record);
$this->redirectRoute('filament.app.resources.projects.index');
} catch (Exception $e) {
Notification::make()
->title($e->getMessage())
->danger()
->send();
}
}),
];
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Web\Pages\Settings\Projects\Widgets;
use App\Models\Project;
use App\Models\User;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
class AddUser extends Widget implements HasForms
{
use InteractsWithForms;
protected static string $view = 'web.components.form';
public Project $project;
public ?int $user;
public function mount(Project $project): void
{
$this->project = $project;
}
public function getFormSchema(): array
{
return [
Section::make()
->heading('Add User')
->schema([
Select::make('user')
->name('user')
->options(fn () => User::query()->pluck('name', 'id'))
->searchable()
->rules(\App\Actions\Projects\AddUser::rules($this->project)['user']),
])
->footerActions([
Action::make('add')
->label('Add')
->action(fn () => $this->submit()),
]),
];
}
public function submit(): void
{
$this->authorize('update', $this->project);
$this->validate();
app(\App\Actions\Projects\AddUser::class)
->add($this->project, [
'user' => $this->user,
]);
Notification::make()
->title('User added!')
->success()
->send();
$this->user = null;
}
public function updated(): void
{
$this->dispatch('userAdded');
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Web\Pages\Settings\Projects\Widgets;
use App\Models\Project;
use App\Models\User;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class ProjectUsersList extends Widget
{
protected $listeners = ['userAdded' => '$refresh'];
public Project $project;
public function mount(Project $project): void
{
$this->project = $project;
}
protected function getTableQuery(): Builder
{
return User::query()->whereHas('projects', function (Builder $query) {
$query->where('project_id', $this->project->id);
});
}
protected function getTableColumns(): array
{
return [
Tables\Columns\TextColumn::make('id')->width('20%'),
Tables\Columns\TextColumn::make('name')->width('20%'),
Tables\Columns\TextColumn::make('email')->width('20%'),
];
}
public function getTable(): Table
{
return $this->table->actions([
Tables\Actions\DeleteAction::make()
->label('Remove')
->modalHeading('Remove user from project')
->visible(function ($record) {
return $this->authorize('update', $this->project)->allowed() && $record->id !== auth()->id();
})
->using(function ($record) {
$this->project->users()->detach($record);
}),
])->paginated(false);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Web\Pages\Settings\Projects\Widgets;
use App\Models\Project;
use App\Web\Pages\Settings\Projects\Settings;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class ProjectsList extends Widget
{
protected function getTableQuery(): Builder
{
return Project::query();
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('created_at_by_timezone')
->label('Created At')
->searchable()
->sortable(),
];
}
public function getTable(): Table
{
return $this->table
->recordUrl(fn (Project $record) => Settings::getUrl(['project' => $record]))
->actions([
Action::make('settings')
->label('Settings')
->icon('heroicon-o-cog-6-tooth')
->authorize(fn ($record) => auth()->user()->can('update', $record))
->url(fn (Project $record) => Settings::getUrl(['project' => $record])),
]);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Web\Pages\Settings\Projects\Widgets;
use App\Models\Project;
use Filament\Forms\Components\Select;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Widgets\Widget;
use Illuminate\Database\Eloquent\Builder;
class SelectProject extends Widget implements HasForms
{
use InteractsWithForms;
protected static string $view = 'web.components.form';
public int|string|null $project;
protected function getFormSchema(): array
{
$options = Project::query()
->where(function (Builder $query) {
if (auth()->user()->isAdmin()) {
return;
}
$query->where('user_id', auth()->id())
->orWhereHas('users', fn ($query) => $query->where('user_id', auth()->id()));
})
->get()
->mapWithKeys(fn ($project) => [$project->id => $project->name])
->toArray();
return [
Select::make('project')
->name('project')
->model($this->project)
->label('Project')
->searchable()
->options($options)
->searchPrompt('Select a project...')
->extraAttributes(['class' => '-mx-2 pointer-choices'])
->selectablePlaceholder(false)
->live(),
];
}
public function updatedProject($value): void
{
$project = Project::query()->findOrFail($value);
$this->authorize('view', $project);
auth()->user()->update(['current_project_id' => $value]);
$this->redirect('/app');
}
public function mount(): void
{
$this->project = auth()->user()->current_project_id;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Web\Pages\Settings\Projects\Widgets;
use App\Models\Project;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
class UpdateProject extends Widget implements HasForms
{
use InteractsWithForms;
protected static string $view = 'web.components.form';
public Project $project;
public string $name;
public function mount(Project $project): void
{
$this->project = $project;
$this->name = $project->name;
}
public function getFormSchema(): array
{
return [
Section::make()
->heading('Project Information')
->schema([
TextInput::make('name')
->name('name')
->label('Name')
->rules(\App\Actions\Projects\UpdateProject::rules($this->project)['name'])
->placeholder('Enter the project name'),
])
->footerActions([
Action::make('save')
->label('Save')
->action(fn () => $this->submit()),
]),
];
}
public function submit(): void
{
$this->authorize('update', $this->project);
$this->validate();
app(\App\Actions\Projects\UpdateProject::class)
->update($this->project, [
'name' => $this->name,
]);
Notification::make()
->title('Project updated successfully!')
->success()
->send();
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Web\Pages\Settings\ServerProviders\Actions;
use App\Actions\ServerProvider\CreateServerProvider;
use App\Enums\ServerProvider;
use Exception;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
class Create
{
public static function form(): array
{
return [
Select::make('provider')
->options(
collect(config('core.server_providers'))
->filter(fn ($provider) => $provider != ServerProvider::CUSTOM)
->mapWithKeys(fn ($provider) => [$provider => $provider])
)
->live()
->reactive()
->rules(CreateServerProvider::rules()['provider']),
TextInput::make('name')
->rules(CreateServerProvider::rules()['name']),
TextInput::make('token')
->label('API Key')
->validationAttribute('API Key')
->visible(fn ($get) => in_array($get('provider'), [
ServerProvider::DIGITALOCEAN,
ServerProvider::LINODE,
ServerProvider::VULTR,
ServerProvider::HETZNER,
]))
->rules(fn ($get) => CreateServerProvider::providerRules($get())['token']),
TextInput::make('key')
->label('Access Key')
->visible(function ($get) {
return $get('provider') == ServerProvider::AWS;
})
->rules(fn ($get) => CreateServerProvider::providerRules($get())['key']),
TextInput::make('secret')
->label('Secret')
->visible(fn ($get) => $get('provider') == ServerProvider::AWS)
->rules(fn ($get) => CreateServerProvider::providerRules($get())['secret']),
Checkbox::make('global')
->label('Is Global (Accessible in all projects)'),
];
}
/**
* @throws Exception
*/
public static function action(array $data): void
{
try {
app(CreateServerProvider::class)->create(auth()->user(), $data);
} catch (Exception $e) {
Notification::make()
->title($e->getMessage())
->danger()
->send();
throw $e;
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Web\Pages\Settings\ServerProviders\Actions;
use App\Actions\ServerProvider\EditServerProvider;
use App\Models\ServerProvider;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\TextInput;
class Edit
{
public static function form(): array
{
return [
TextInput::make('name')
->label('Name')
->rules(EditServerProvider::rules()['name']),
Checkbox::make('global')
->label('Is Global (Accessible in all projects)'),
];
}
public static function action(ServerProvider $provider, array $data): void
{
app(EditServerProvider::class)->edit($provider, auth()->user(), $data);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Web\Pages\Settings\ServerProviders;
use App\Enums\ServerProvider;
use App\Web\Traits\PageHasWidgets;
use Filament\Actions\CreateAction;
use Filament\Pages\Page;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
use PageHasWidgets;
protected static ?string $navigationGroup = 'Settings';
protected static ?string $slug = 'settings/server-providers';
protected static ?string $title = 'Server Providers';
protected static ?string $navigationIcon = 'heroicon-o-server-stack';
protected static ?int $navigationSort = 5;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', ServerProvider::class) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\ServerProvidersList::class],
];
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Connect')
->modalHeading('Connect to a Server Provider')
->modalSubmitActionLabel('Connect')
->createAnother(false)
->form(Actions\Create::form())
->authorize('create', ServerProvider::class)
->modalWidth(MaxWidth::Medium)
->using(fn (array $data) => Actions\Create::action($data)),
];
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Web\Pages\Settings\ServerProviders\Widgets;
use App\Actions\ServerProvider\DeleteServerProvider;
use App\Models\ServerProvider;
use App\Web\Pages\Settings\ServerProviders\Actions\Edit;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class ServerProvidersList extends Widget
{
protected function getTableQuery(): Builder
{
return ServerProvider::getByProjectId(auth()->user()->current_project_id);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
ImageColumn::make('image_url')
->label('Provider')
->size(24),
TextColumn::make('name')
->default(fn ($record) => $record->profile)
->label('Name')
->searchable()
->sortable(),
TextColumn::make('id')
->label('Global')
->badge()
->color(fn ($record) => $record->project_id ? 'gray' : 'success')
->formatStateUsing(function (ServerProvider $record) {
return $record->project_id ? 'No' : 'Yes';
}),
TextColumn::make('created_at_by_timezone')
->label('Created At')
->searchable()
->sortable(),
];
}
public function getTable(): Table
{
return $this->table->actions([
EditAction::make('edit')
->label('Edit')
->modalHeading('Edit Server Provider')
->mutateRecordDataUsing(function (array $data, ServerProvider $record) {
return [
'name' => $record->profile,
'global' => $record->project_id === null,
];
})
->form(Edit::form())
->authorize(fn (ServerProvider $record) => auth()->user()->can('update', $record))
->using(fn (array $data, ServerProvider $record) => Edit::action($record, $data))
->modalWidth(MaxWidth::Medium),
DeleteAction::make('delete')
->label('Delete')
->modalHeading('Delete Server Provider')
->authorize(fn (ServerProvider $record) => auth()->user()->can('delete', $record))
->using(function (array $data, ServerProvider $record) {
app(DeleteServerProvider::class)->delete($record);
}),
]);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Web\Pages\Settings\Users;
use App\Actions\User\CreateUser;
use App\Models\User;
use App\Web\Traits\PageHasWidgets;
use Filament\Actions\CreateAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Pages\Page;
use Filament\Support\Enums\MaxWidth;
class Index extends Page
{
use PageHasWidgets;
protected static ?string $navigationGroup = 'Settings';
protected static ?string $slug = 'users';
protected static ?string $navigationIcon = 'heroicon-o-users';
protected static ?int $navigationSort = 3;
protected static ?string $title = 'Users';
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', User::class) ?? false;
}
public function getWidgets(): array
{
return [
[Widgets\UsersList::class],
];
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Create User')
->authorize('create', User::class)
->action(function (array $data) {
$user = app(CreateUser::class)->create($data);
$this->dispatch('$refresh');
return $user;
})
->form(function (Form $form) {
$rules = CreateUser::rules();
return $form
->schema([
TextInput::make('name')
->rules($rules['name']),
TextInput::make('email')
->rules($rules['email']),
TextInput::make('password')
->rules($rules['password']),
Select::make('role')
->rules($rules['role'])
->options(collect(config('core.user_roles'))->mapWithKeys(fn ($role) => [$role => $role])),
])
->columns(1);
})
->modalWidth(MaxWidth::Large),
];
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Web\Pages\Settings\Users\Widgets;
use App\Actions\User\UpdateProjects;
use App\Actions\User\UpdateUser;
use App\Models\Project;
use App\Models\User;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class UsersList extends Widget
{
protected $listeners = ['$refresh'];
protected function getTableQuery(): Builder
{
return User::query();
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('email')
->searchable()
->sortable(),
TextColumn::make('timezone'),
TextColumn::make('created_at_by_timezone')
->label('Created At')
->searchable()
->sortable(),
TextColumn::make('role'),
];
}
public function getTable(): Table
{
return $this->table
->actions([
EditAction::make('edit')
->authorize(fn ($record) => auth()->user()->can('update', $record))
->using(function ($record, array $data) {
app(UpdateUser::class)->update($record, $data);
})
->form(function (Form $form, $record) {
return $form
->schema([
TextInput::make('name')
->rules(UpdateUser::rules($record)['name']),
TextInput::make('email')
->rules(UpdateUser::rules($record)['email']),
Select::make('timezone')
->searchable()
->options(
collect(timezone_identifiers_list())
->mapWithKeys(fn ($timezone) => [$timezone => $timezone])
)
->rules(UpdateUser::rules($record)['timezone']),
Select::make('role')
->options(
collect(config('core.user_roles'))
->mapWithKeys(fn ($role) => [$role => $role])
)
->rules(UpdateUser::rules($record)['role']),
])
->columns(1);
})
->modalWidth(MaxWidth::Large),
Action::make('update-projects')
->label('Projects')
->icon('heroicon-o-rectangle-stack')
->authorize(fn ($record) => auth()->user()->can('update', $record))
->action(function ($record, array $data) {
app(UpdateProjects::class)->update($record, $data);
Notification::make()
->title('Projects Updated')
->success()
->send();
})
->form(function (Form $form, $record) {
return $form
->schema([
CheckboxList::make('projects')
->options(Project::query()->pluck('name', 'id')->toArray())
->searchable()
->default($record->projects->pluck('id')->toArray())
->rules(UpdateProjects::rules()['projects.*']),
])
->columns(1);
})
->modalSubmitActionLabel('Save')
->modalWidth(MaxWidth::Large),
DeleteAction::make()
->authorize(fn ($record) => auth()->user()->can('delete', $record)),
]);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Web\Traits;
use App\Models\Server;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
trait BelongsToServers
{
public static function getUrl(string $name = 'index', array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
{
if (! isset($parameters['server'])) {
$parameters['server'] = request()->route('server') ?? 0;
}
return parent::getUrl($name, $parameters, $isAbsolute, $panel, $tenant);
}
public static function getServerFromRoute(): Server
{
$server = request()->route('server');
if (! $server) {
$server = Route::getRoutes()->match(Request::create(url()->previous()))->parameter('server');
}
if (! $server instanceof Server) {
$server = Server::query()->find($server);
}
if (! $server) {
$server = new Server;
}
return $server;
}
public static function canViewAny(): bool
{
return static::can('viewAny');
return auth()->user()->can('viewAny', [static::getModel(), static::getServerFromRoute()]);
}
public static function canCreate(): bool
{
return auth()->user()->can('create', [static::getModel(), static::getServerFromRoute()]);
}
public static function authorizeViewAny(): void
{
Gate::authorize('viewAny', [static::getModel(), static::getServerFromRoute()]);
}
public static function authorizeCreate(): void
{
Gate::authorize('create', [static::getModel(), static::getServerFromRoute()]);
}
public static function authorizeEdit(Model $record): void
{
Gate::authorize('update', [$record, static::getServerFromRoute()]);
}
public static function authorizeView(Model $record): void
{
Gate::authorize('view', [$record, static::getServerFromRoute()]);
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Web\Traits;
trait PageHasCluster
{
// public function getMaxContentWidth(): ?string
// {
// return 'full';
// }
public function getSubNavigation(): array
{
if (filled($cluster = static::getCluster())) {
return $this->generateNavigationItems($cluster::getClusteredComponents());
}
return [];
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Web\Traits;
use App\Models\Site;
use App\Web\Pages\Servers\Settings;
use App\Web\Pages\Servers\Sites\Index;
use App\Web\Pages\Servers\View;
use App\Web\Pages\Servers\Widgets\ServerSummary;
use Filament\Navigation\NavigationItem;
use Illuminate\Contracts\Support\Htmlable;
trait PageHasServer
{
public function getTitle(): string|Htmlable
{
return static::$title.' - '.$this->server->name;
}
public function getSubNavigation(): array
{
$items = [];
if (auth()->user()?->can('view', $this->server)) {
$items[] = NavigationItem::make('Overview')
->icon('heroicon-o-chart-pie')
->isActiveWhen(fn () => request()->routeIs(View::getRouteName()))
->url(View::getUrl(parameters: ['server' => $this->server]));
}
if (auth()->user()?->can('viewAny', [Site::class, $this->server])) {
$items[] = NavigationItem::make('Sites')
->icon('heroicon-o-globe-alt')
->isActiveWhen(fn () => request()->routeIs(Index::getRouteName().'*'))
->url(Index::getUrl(parameters: ['server' => $this->server]));
}
if (auth()->user()?->can('update', $this->server)) {
$items[] = NavigationItem::make('Settings')
->icon('heroicon-o-cog-6-tooth')
->isActiveWhen(fn () => request()->routeIs(Settings::getRouteName().'*'))
->url(Settings::getUrl(parameters: ['server' => $this->server]));
}
return $items;
}
protected function getHeaderWidgets(): array
{
return [
ServerSummary::make([
'server' => $this->server,
]),
];
}
public function getHeaderWidgetsColumns(): int|string|array
{
return 1;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Web\Traits;
use Illuminate\View\ComponentAttributeBag;
trait PageHasWidgets
{
protected array $extraAttributes = [
'wire:poll.5s' => '$dispatch(\'$refresh\')',
];
protected function getExtraAttributes(): array
{
return $this->extraAttributes;
}
public function getView(): string
{
return 'web.components.page';
}
public function getExtraAttributesBag(): ComponentAttributeBag
{
return new ComponentAttributeBag($this->getExtraAttributes());
}
abstract public function getWidgets(): array;
}