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,33 @@
<?php
namespace App\Actions\Projects;
use App\Models\Project;
use App\Models\User;
use Illuminate\Database\Query\Builder;
use Illuminate\Validation\Rule;
class AddUser
{
public function add(Project $project, array $input): void
{
/** @var User $user */
$user = User::query()->findOrFail($input['user']);
$project->users()->detach($user);
$project->users()->attach($user);
}
public static function rules(Project $project): array
{
return [
'user' => [
'required',
Rule::exists('users', 'id'),
Rule::unique('user_project', 'user_id')->where(function (Builder $query) use ($project) {
$query->where('project_id', $project->id);
}),
],
];
}
}

View File

@ -27,15 +27,21 @@ public function create(User $user, array $input): Project
return $project;
}
private function validate(array $input): void
public static function rules(): array
{
Validator::make($input, [
return [
'name' => [
'required',
'string',
'max:255',
'unique:projects,name',
'lowercase:projects,name',
],
])->validate();
];
}
private function validate(array $input): void
{
Validator::make($input, self::rules())->validate();
}
}

View File

@ -23,15 +23,21 @@ public function update(Project $project, array $input): Project
return $project;
}
private function validate(Project $project, array $input): void
public static function rules(Project $project): array
{
Validator::make($input, [
return [
'name' => [
'required',
'string',
'max:255',
Rule::unique('projects')->where('user_id', $project->user_id)->ignore($project->id),
Rule::unique('projects', 'name')->ignore($project->id),
'lowercase:projects,name',
],
])->validate();
];
}
private function validate(Project $project, array $input): void
{
Validator::make($input, self::rules($project))->validate();
}
}

View File

@ -5,7 +5,6 @@
use App\Enums\FirewallRuleStatus;
use App\Enums\ServerProvider;
use App\Enums\ServerStatus;
use App\Exceptions\ServerProviderError;
use App\Facades\Notifier;
use App\Models\Server;
use App\Models\User;
@ -16,10 +15,8 @@
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Throwable;
class CreateServer
@ -29,8 +26,6 @@ class CreateServer
*/
public function create(User $creator, array $input): Server
{
$this->validateInputs($input);
$server = new Server([
'project_id' => $creator->currentProject->id,
'user_id' => $creator->id,
@ -56,12 +51,8 @@ public function create(User $creator, array $input): Server
$server->provider_id = $input['server_provider'];
}
// validate type
$this->validateType($server, $input);
$server->type_data = $server->type()->data($input);
// validate provider
$this->validateProvider($server, $input);
$server->provider_data = $server->provider()->data($input);
// save
@ -85,11 +76,6 @@ public function create(User $creator, array $input): Server
} catch (Exception $e) {
$server->provider()->delete();
DB::rollBack();
if ($e instanceof ServerProviderError) {
throw ValidationException::withMessages([
'provider' => __('Provider Error: ').$e->getMessage(),
]);
}
throw $e;
}
}
@ -126,59 +112,82 @@ function () use ($server) {
$bus->onConnection('ssh')->dispatch();
}
/**
* @throws ValidationException
*/
private function validateInputs(array $input): void
public static function rules(array $input): array
{
$rules = [
'provider' => 'required|in:'.implode(',', config('core.server_providers')),
'name' => 'required',
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
'provider' => [
'required',
Rule::in(config('core.server_providers')),
],
'name' => [
'required',
],
'os' => [
'required',
Rule::in(config('core.operating_systems')),
],
'type' => [
'required',
Rule::in(config('core.server_types')),
],
'server_provider' => [
Rule::when(function () use ($input) {
return $input['provider'] != ServerProvider::CUSTOM;
}, [
'required',
'exists:server_providers,id,user_id,'.auth()->user()->id,
]),
],
'ip' => [
Rule::when(function () use ($input) {
return $input['provider'] == ServerProvider::CUSTOM;
}, [
'required',
new RestrictedIPAddressesRule,
]),
],
'port' => [
Rule::when(function () use ($input) {
return $input['provider'] == ServerProvider::CUSTOM;
}, [
'required',
'numeric',
'min:1',
'max:65535',
]),
],
];
Validator::make($input, $rules)->validate();
if ($input['provider'] != 'custom') {
$rules['server_provider'] = 'required|exists:server_providers,id,user_id,'.auth()->user()->id;
}
if ($input['provider'] == 'custom') {
$rules['ip'] = [
'required',
new RestrictedIPAddressesRule(),
];
$rules['port'] = [
'required',
'numeric',
'min:1',
'max:65535',
];
}
Validator::make($input, $rules)->validate();
return array_merge($rules, self::typeRules($input), self::providerRules($input));
}
/**
* @throws ValidationException
*/
private function validateType(Server $server, array $input): void
private static function typeRules(array $input): array
{
Validator::make($input, $server->type()->createRules($input))
->validate();
if (! isset($input['type']) || ! in_array($input['type'], config('core.server_types'))) {
return [];
}
$server = new Server(['type' => $input['type']]);
return $server->type()->createRules($input);
}
/**
* @throws ValidationException
*/
private function validateProvider(Server $server, array $input): void
private static function providerRules(array $input): array
{
Validator::make($input, $server->provider()->createRules($input))
->validate();
if (
! isset($input['provider']) ||
! in_array($input['provider'], config('core.server_providers')) ||
$input['provider'] == ServerProvider::CUSTOM
) {
return [];
}
$server = new Server([
'provider' => $input['provider'],
'provider_id' => $input['server_provider'],
]);
return $server->provider()->createRules($input);
}
private function createFirewallRules(Server $server): void

View File

@ -4,7 +4,7 @@
use App\Models\Server;
use App\ValidationRules\RestrictedIPAddressesRule;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class EditServer
@ -14,8 +14,6 @@ class EditServer
*/
public function edit(Server $server, array $input): Server
{
$this->validate($input);
$checkConnection = false;
if (isset($input['name'])) {
$server->name = $input['name'];
@ -41,15 +39,24 @@ public function edit(Server $server, array $input): Server
return $server;
}
/**
* @throws ValidationException
*/
protected function validate(array $input): void
public static function rules(Server $server): array
{
Validator::make($input, [
'ip' => [
new RestrictedIPAddressesRule(),
return [
'name' => [
'string',
'max:255',
Rule::unique('servers')->where('project_id', $server->project_id)->ignore($server->id),
],
])->validateWithBag('editServer');
'ip' => [
'string',
new RestrictedIPAddressesRule,
Rule::unique('servers')->where('project_id', $server->project_id)->ignore($server->id),
],
'port' => [
'integer',
'min:1',
'max:65535',
],
];
}
}

View File

@ -6,7 +6,6 @@
use App\Models\User;
use App\ServerProviders\ServerProvider as ServerProviderContract;
use Exception;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
@ -17,11 +16,7 @@ class CreateServerProvider
*/
public function create(User $user, array $input): ServerProvider
{
$this->validateInput($input);
$provider = $this->getProvider($input['provider']);
$this->validateProvider($provider, $input);
$provider = static::getProvider($input['provider']);
try {
$provider->connect($input);
@ -33,7 +28,7 @@ public function create(User $user, array $input): ServerProvider
]);
}
$serverProvider = new ServerProvider();
$serverProvider = new ServerProvider;
$serverProvider->user_id = $user->id;
$serverProvider->profile = $input['name'];
$serverProvider->provider = $input['provider'];
@ -44,19 +39,16 @@ public function create(User $user, array $input): ServerProvider
return $serverProvider;
}
private function getProvider($name): ServerProviderContract
private static function getProvider($name): ServerProviderContract
{
$providerClass = config('core.server_providers_class.'.$name);
return new $providerClass();
return new $providerClass;
}
/**
* @throws ValidationException
*/
private function validateInput(array $input): void
public static function rules(): array
{
Validator::make($input, [
return [
'name' => [
'required',
],
@ -65,14 +57,11 @@ private function validateInput(array $input): void
Rule::in(config('core.server_providers')),
Rule::notIn('custom'),
],
])->validate();
];
}
/**
* @throws ValidationException
*/
private function validateProvider(ServerProviderContract $provider, array $input): void
public static function providerRules(array $input): array
{
Validator::make($input, $provider->credentialValidationRules($input))->validate();
return static::getProvider($input['provider'])->credentialValidationRules($input);
}
}

View File

@ -4,31 +4,23 @@
use App\Models\ServerProvider;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class EditServerProvider
{
public function edit(ServerProvider $serverProvider, User $user, array $input): void
{
$this->validate($input);
$serverProvider->profile = $input['name'];
$serverProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
$serverProvider->save();
}
/**
* @throws ValidationException
*/
private function validate(array $input): void
public static function rules(): array
{
$rules = [
return [
'name' => [
'required',
],
];
Validator::make($input, $rules)->validate();
}
}

View File

@ -101,22 +101,26 @@ public function create(Server $server, array $input): Site
/**
* @throws ValidationException
*/
private function validateInputs(Server $server, array $input): void
public static function rules(array $input): void
{
$rules = [
'server_id' => [
'required',
'exists:servers,id',
],
'type' => [
'required',
Rule::in(config('core.site_types')),
],
'domain' => [
'required',
new DomainRule(),
new DomainRule,
Rule::unique('sites', 'domain')->where(function ($query) use ($server) {
return $query->where('server_id', $server->id);
}),
],
'aliases.*' => [
new DomainRule(),
new DomainRule,
],
];

View File

@ -27,7 +27,12 @@ public function create(array $input): User
private function validate(array $input): void
{
Validator::make($input, [
Validator::make($input, self::rules())->validate();
}
public static function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:8',
@ -35,6 +40,6 @@ private function validate(array $input): void
'required',
Rule::in([UserRole::ADMIN, UserRole::USER]),
],
])->validate();
];
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Actions\User;
use App\Models\Project;
use App\Models\User;
use Illuminate\Validation\Rule;
class UpdateProjects
{
public function update(User $user, array $input): void
{
$this->validate($input);
$user->projects()->sync($input['projects']);
if ($user->currentProject && !$user->projects->contains($user->currentProject)) {
$user->current_project_id = null;
$user->save();
}
/** @var Project $firstProject */
$firstProject = $user->projects->first();
if (!$user->currentProject && $firstProject) {
$user->current_project_id = $firstProject->id;
$user->save();
}
}
private function validate(array $input): void
{
validator($input, self::rules())->validate();
}
public static function rules(): array
{
return [
'projects.*' => [
'required',
Rule::exists('projects', 'id'),
],
];
}
}

View File

@ -9,7 +9,7 @@
class UpdateUser
{
public function update(User $user, array $input): void
public function update(User $user, array $input): User
{
$this->validate($user, $input);
@ -18,18 +18,29 @@ public function update(User $user, array $input): void
$user->timezone = $input['timezone'];
$user->role = $input['role'];
if (isset($input['password']) && $input['password'] !== null) {
if (isset($input['password'])) {
$user->password = bcrypt($input['password']);
}
$user->save();
return $user;
}
private function validate(User $user, array $input): void
{
Validator::make($input, [
Validator::make($input, self::rules($user))->validate();
}
public static function rules(User $user): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'email' => [
'required',
'email', 'max:255',
Rule::unique('users', 'email')->ignore($user->id)
],
'timezone' => [
'required',
Rule::in(timezone_identifiers_list()),
@ -37,12 +48,7 @@ private function validate(User $user, array $input): void
'role' => [
'required',
Rule::in([UserRole::ADMIN, UserRole::USER]),
function ($attribute, $value, $fail) use ($user) {
if ($user->is(auth()->user()) && $value !== $user->role) {
$fail('You cannot change your own role');
}
},
],
])->validate();
];
}
}

View File

@ -3,23 +3,22 @@
namespace App\Actions\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class UpdateUserPassword
{
/**
* Validate and update the user's password.
*/
public function update($user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current-password'],
'password' => ['required', 'string'],
'password_confirmation' => ['required', 'same:password'],
])->validate();
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
public static function rules(): array
{
return [
'current_password' => ['required', 'string', 'current-password'],
'password' => ['required', 'string'],
'password_confirmation' => ['required', 'same:password'],
];
}
}

View File

@ -3,28 +3,12 @@
namespace App\Actions\User;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class UpdateUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @throws Exception
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'timezone' => [
'required',
Rule::in(timezone_identifiers_list()),
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email) {
$this->updateVerifiedUser($user, $input);
} else {
@ -36,6 +20,18 @@ public function update(User $user, array $input): void
}
}
public static function rules(User $user): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
'timezone' => [
'required',
Rule::in(timezone_identifiers_list()),
],
];
}
/**
* Update the given verified user's profile information.
*/

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Settings;
use App\Actions\User\CreateUser;
use App\Actions\User\UpdateProjects;
use App\Actions\User\UpdateUser;
use App\Facades\Toast;
use App\Helpers\HtmxResponse;
@ -48,26 +49,7 @@ public function update(User $user, Request $request): RedirectResponse
public function updateProjects(User $user, Request $request): HtmxResponse
{
$this->validate($request, [
'projects.*' => [
'required',
Rule::exists('projects', 'id'),
],
]);
$user->projects()->sync($request->projects);
if ($user->currentProject && ! $user->projects->contains($user->currentProject)) {
$user->current_project_id = null;
$user->save();
}
/** @var Project $firstProject */
$firstProject = $user->projects->first();
if (! $user->currentProject && $firstProject) {
$user->current_project_id = $firstProject->id;
$user->save();
}
app(UpdateProjects::class)->update($user, $request->input());
Toast::success('Projects updated successfully');

View File

@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\HasTimezoneTimestamps;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
@ -12,6 +13,8 @@
*/
abstract class AbstractModel extends Model
{
use HasTimezoneTimestamps;
public function jsonUpdate(string $field, string $key, mixed $value, bool $save = true): void
{
$current = $this->{$field};

View File

@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\HasTimezoneTimestamps;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -18,12 +19,14 @@
* @property Carbon $updated_at
* @property User $user
* @property Collection<Server> $servers
* @property Collection<User> $users
* @property Collection<NotificationChannel> $notificationChannels
* @property Collection<SourceControl> $sourceControls
*/
class Project extends Model
{
use HasFactory;
use HasTimezoneTimestamps;
protected $fillable = [
'user_id',

View File

@ -3,12 +3,14 @@
namespace App\Models;
use App\Actions\Server\CheckConnection;
use App\Enums\ServerStatus;
use App\Enums\ServiceStatus;
use App\Facades\SSH;
use App\ServerTypes\ServerType;
use App\SSH\Cron\Cron;
use App\SSH\OS\OS;
use App\SSH\Systemd\Systemd;
use App\Support\Testing\SSHFake;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -56,6 +58,7 @@
* @property Backup[] $backups
* @property Queue[] $daemons
* @property SshKey[] $sshKeys
* @property Tag[] $tags
* @property string $hostname
* @property int $updates
* @property Carbon $last_update_check
@ -138,6 +141,29 @@ public static function boot(): void
});
}
public static array $statusColors = [
ServerStatus::READY => 'success',
ServerStatus::INSTALLING => 'warning',
ServerStatus::DISCONNECTED => 'gray',
ServerStatus::INSTALLATION_FAILED => 'danger',
ServerStatus::UPDATING => 'warning',
];
public function isReady(): bool
{
return $this->status === ServerStatus::READY;
}
public function isInstalling(): bool
{
return in_array($this->status, [ServerStatus::INSTALLING, ServerStatus::INSTALLATION_FAILED]);
}
public function isInstallationFailed(): bool
{
return $this->status === ServerStatus::INSTALLATION_FAILED;
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class, 'project_id');
@ -268,7 +294,7 @@ public function defaultService($type): ?Service
return $service;
}
public function ssh(?string $user = null): mixed
public function ssh(?string $user = null): \App\Helpers\SSH|SSHFake
{
return SSH::init($this, $user);
}
@ -295,7 +321,7 @@ public function provider(): \App\ServerProviders\ServerProvider
{
$providerClass = config('core.server_providers_class')[$this->provider];
return new $providerClass($this);
return new $providerClass($this->serverProvider, $this);
}
public function webserver(?string $version = null): ?Service
@ -404,4 +430,13 @@ public function checkForUpdates(): void
$this->last_update_check = now();
$this->save();
}
public function getAvailableUpdatesAttribute(?int $value): int
{
if (! $value) {
return 0;
}
return $value;
}
}

View File

@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Cache;
/**
* @property int $user_id
@ -15,6 +16,9 @@
* @property bool $connected
* @property User $user
* @property ?int $project_id
* @property Server[] $servers
* @property Project $project
* @property string $image_url
*/
class ServerProvider extends AbstractModel
{
@ -51,6 +55,13 @@ public function servers(): HasMany
return $this->hasMany(Server::class, 'provider_id');
}
public function provider(): \App\ServerProviders\ServerProvider
{
$providerClass = config('core.server_providers_class')[$this->provider];
return new $providerClass($this);
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
@ -59,7 +70,54 @@ public function project(): BelongsTo
public static function getByProjectId(int $projectId): Builder
{
return self::query()
->where('project_id', $projectId)
->orWhereNull('project_id');
->where(function (Builder $query) use ($projectId) {
$query->where('project_id', $projectId)
->orWhereNull('project_id');
});
}
public function getImageUrlAttribute(): string
{
return url('/static/images/'.$this->provider.'.svg');
}
public static function regions(?int $id): array
{
if (! $id) {
return [];
}
$profile = self::find($id);
if (! $profile) {
return [];
}
if (Cache::get('regions-'.$id)) {
return Cache::get('regions-'.$id);
}
$regions = $profile->provider()->regions();
Cache::put('regions-'.$id, $regions, 600);
return $regions;
}
public static function plans(?int $id, ?string $region): array
{
if (! $id) {
return [];
}
$profile = self::find($id);
if (! $profile) {
return [];
}
if (Cache::get('plans-'.$id.'-'.$region)) {
return Cache::get('plans-'.$id.'-'.$region);
}
$plans = $profile->provider()->plans($region);
Cache::put('plans-'.$id.'-'.$region, $plans, 600);
return $plans;
}
}

View File

@ -2,10 +2,12 @@
namespace App\Models;
use App\Enums\SiteStatus;
use App\Exceptions\SourceControlIsNotConnected;
use App\Exceptions\SSHError;
use App\SiteTypes\SiteType;
use App\SSH\Services\Webserver\Webserver;
use App\Traits\HasProjectThroughServer;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -44,6 +46,7 @@
class Site extends AbstractModel
{
use HasFactory;
use HasProjectThroughServer;
protected $fillable = [
'server_id',
@ -73,6 +76,13 @@ class Site extends AbstractModel
'source_control_id' => 'integer',
];
public static array $statusColors = [
SiteStatus::READY => 'success',
SiteStatus::INSTALLING => 'warning',
SiteStatus::INSTALLATION_FAILED => 'danger',
SiteStatus::DELETING => 'danger',
];
public static function boot(): void
{
parent::boot();

View File

@ -3,8 +3,13 @@
namespace App\Models;
use App\Enums\UserRole;
use App\Traits\HasTimezoneTimestamps;
use Carbon\Carbon;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
@ -34,10 +39,13 @@
* @property Project $currentProject
* @property Collection<Project> $projects
* @property string $role
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class User extends Authenticatable
class User extends Authenticatable implements HasTenants
{
use HasFactory;
use HasTimezoneTimestamps;
use Notifiable;
use TwoFactorAuthenticatable;
@ -111,6 +119,16 @@ public function projects(): BelongsToMany
return $this->belongsToMany(Project::class, 'user_project')->withTimestamps();
}
public function getTenants(Panel $panel): Collection
{
return $this->projects;
}
public function canAccessTenant(Model $tenant): bool
{
return $this->projects()->whereKey($tenant)->exists();
}
public function currentProject(): HasOne
{
return $this->HasOne(Project::class, 'id', 'current_project_id');
@ -121,7 +139,7 @@ public function createDefaultProject(): Project
$project = $this->projects()->first();
if (! $project) {
$project = new Project();
$project = new Project;
$project->name = 'default';
$project->save();

View File

@ -2,7 +2,6 @@
namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Project;
use App\Models\User;
@ -10,26 +9,26 @@ class ProjectPolicy
{
public function viewAny(User $user): bool
{
return $user->role === UserRole::ADMIN;
return $user->isAdmin();
}
public function view(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
return $user->isAdmin() || $project->users->contains($user);
}
public function create(User $user): bool
{
return $user->role === UserRole::ADMIN;
return $user->isAdmin();
}
public function update(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN;
return $user->isAdmin();
}
public function delete(User $user, Project $project): bool
{
return $user->role === UserRole::ADMIN;
return $user->isAdmin();
}
}

View File

@ -2,40 +2,38 @@
namespace App\Policies;
use App\Enums\UserRole;
use App\Models\Project;
use App\Models\Server;
use App\Models\User;
class ServerPolicy
{
public function viewAny(User $user, Project $project): bool
public function viewAny(User $user): bool
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
return $user->isAdmin() || $user->currentProject?->users->contains($user);
}
public function view(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
return $user->isAdmin() || $server->project->users->contains($user);
}
public function create(User $user, Project $project): bool
public function create(User $user): bool
{
return $user->role === UserRole::ADMIN || $project->users->contains($user);
return $user->isAdmin() || $user->currentProject?->users->contains($user);
}
public function update(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
return $user->isAdmin() || $server->project->users->contains($user);
}
public function delete(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
return $user->isAdmin() || $server->project->users->contains($user);
}
public function manage(User $user, Server $server): bool
{
return $user->role === UserRole::ADMIN || $server->project->users->contains($user);
return $user->isAdmin() || $server->project->users->contains($user);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Policies;
use App\Models\ServerProvider;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ServerProviderPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return $user->isAdmin();
}
public function view(User $user, ServerProvider $serverProvider): bool
{
return $user->isAdmin();
}
public function create(User $user): bool
{
return $user->isAdmin();
}
public function update(User $user, ServerProvider $serverProvider): bool
{
return $user->isAdmin();
}
public function delete(User $user, ServerProvider $serverProvider): bool
{
return $user->isAdmin();
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Policies;
use App\Models\Server;
use App\Models\Site;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class SitePolicy
{
use HandlesAuthorization;
public function viewAny(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
}
public function view(User $user, Site $site): bool
{
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
$site->server->isReady();
}
public function create(User $user, Server $server): bool
{
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
}
public function update(User $user, Site $site): bool
{
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
$site->server->isReady();
}
public function delete(User $user, Site $site): bool
{
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
$site->server->isReady();
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return $user->isAdmin();
}
public function view(User $user, User $model): bool
{
return $user->isAdmin();
}
public function create(User $user): bool
{
return $user->isAdmin();
}
public function update(User $user, User $model): bool
{
return $user->isAdmin();
}
public function delete(User $user, User $model): bool
{
return $user->isAdmin();
}
}

View File

@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string
*/
public const HOME = '/servers';
public const HOME = '/app';
/**
* Define your route model bindings, pattern filters, and other route configuration.

View File

@ -0,0 +1,99 @@
<?php
namespace App\Providers;
use App\Web\Pages\Settings\Projects\Widgets\SelectProject;
use Exception;
use Filament\Facades\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Panel;
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use Livewire\Livewire;
class WebServiceProvider extends ServiceProvider
{
/**
* @throws Exception
*/
public function register(): void
{
Filament::registerPanel($this->panel(Panel::make()));
}
public function boot(): void
{
FilamentView::registerRenderHook(
PanelsRenderHook::SIDEBAR_NAV_START,
fn () => Livewire::mount(SelectProject::class)
);
FilamentColor::register([
'slate' => Color::Slate,
'gray' => Color::Zinc,
'red' => Color::Red,
'orange' => Color::Orange,
'amber' => Color::Amber,
'yellow' => Color::Yellow,
'lime' => Color::Lime,
'green' => Color::Green,
'emerald' => Color::Emerald,
'teal' => Color::Teal,
'cyan' => Color::Cyan,
'sky' => Color::Sky,
'blue' => Color::Blue,
'indigo' => Color::Indigo,
'violet' => Color::Violet,
'purple' => Color::Purple,
'fuchsia' => Color::Fuchsia,
'pink' => Color::Pink,
'rose' => Color::Rose,
]);
}
/**
* @throws Exception
*/
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('app')
->path('app')
->passwordReset()
->colors([
'primary' => Color::Indigo,
])
->viteTheme('resources/css/filament/app/theme.css')
->brandLogo(fn () => view('web.components.brand'))
->brandLogoHeight('30px')
->discoverPages(in: app_path('Web/Pages'), for: 'App\\Web\\Pages')
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
])
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
->globalSearchFieldKeyBindingSuffix();
}
}

View File

@ -10,7 +10,6 @@
use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Throwable;
class AWS extends AbstractProvider
@ -21,12 +20,7 @@ class AWS extends AbstractProvider
public function createRules(array $input): array
{
$rules = [
'os' => [
'required',
Rule::in(config('core.operating_systems')),
],
];
$rules = [];
// plans
$plans = [];
foreach (config('serverproviders.aws.plans') as $plan) {
@ -82,14 +76,18 @@ public function connect(?array $credentials = null): bool
}
}
public function plans(): array
public function plans(?string $region): array
{
return config('serverproviders.aws.plans');
return collect(config('serverproviders.aws.plans'))
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
->toArray();
}
public function regions(): array
{
return config('serverproviders.aws.regions');
return collect(config('serverproviders.aws.regions'))
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
->toArray();
}
public function create(): void

View File

@ -3,15 +3,19 @@
namespace App\ServerProviders;
use App\Models\Server;
use App\Models\ServerProvider as Provider;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
abstract class AbstractProvider implements ServerProvider
{
protected ?Provider $serverProvider;
protected ?Server $server;
public function __construct(?Server $server = null)
public function __construct(?Provider $serverProvider = null, ?Server $server = null)
{
$this->serverProvider = $serverProvider;
$this->server = $server;
}

View File

@ -15,7 +15,7 @@ public function createRules(array $input): array
'ip' => [
'required',
Rule::unique('servers', 'ip'),
new RestrictedIPAddressesRule(),
new RestrictedIPAddressesRule,
],
'port' => [
'required',
@ -46,7 +46,7 @@ public function connect(?array $credentials = null): bool
return true;
}
public function plans(): array
public function plans(?string $region): array
{
return [];
}

View File

@ -16,9 +16,7 @@ class DigitalOcean extends AbstractProvider
public function createRules(array $input): array
{
$rules = [
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
];
$rules = [];
// plans
$plans = [];
foreach (config('serverproviders.digitalocean.plans') as $plan) {
@ -70,14 +68,18 @@ public function connect(?array $credentials = null): bool
return true;
}
public function plans(): array
public function plans(?string $region): array
{
return config('serverproviders.digitalocean.plans');
return collect(config('serverproviders.digitalocean.plans'))
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
->toArray();
}
public function regions(): array
{
return config('serverproviders.digitalocean.regions');
return collect(config('serverproviders.digitalocean.regions'))
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
->toArray();
}
/**

View File

@ -6,6 +6,8 @@
use App\Exceptions\ServerProviderError;
use App\Facades\Notifier;
use App\Notifications\FailedToDeleteServerFromProvider;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
@ -16,7 +18,6 @@ class Hetzner extends AbstractProvider
public function createRules(array $input): array
{
return [
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
'plan' => 'required',
'region' => 'required',
];
@ -46,6 +47,7 @@ public function data(array $input): array
/**
* @throws CouldNotConnectToProvider
* @throws ConnectionException
*/
public function connect(?array $credentials = null): bool
{
@ -57,18 +59,53 @@ public function connect(?array $credentials = null): bool
return true;
}
public function plans(): array
public function plans(?string $region): array
{
return config('serverproviders.hetzner.plans');
try {
$plans = Http::withToken($this->serverProvider->credentials['token'])
->get($this->apiUrl.'/server_types', ['per_page' => 50])
->json();
return collect($plans['server_types'])->filter(function ($type) use ($region) {
return collect($type['prices'])->filter(function ($price) use ($region) {
return $price['location'] === $region;
});
})
->mapWithKeys(function ($value) {
return [
$value['name'] => __('server_providers.plan', [
'name' => $value['name'],
'cpu' => $value['cores'],
'architecture' => $value['architecture'],
'memory' => $value['memory'],
'disk' => $value['disk'],
]),
];
})
->toArray();
} catch (Exception) {
return [];
}
}
public function regions(): array
{
return config('serverproviders.hetzner.regions');
try {
$regions = Http::withToken($this->serverProvider->credentials['token'])
->get($this->apiUrl.'/locations', ['per_page' => 50])
->json();
return collect($regions['locations'])
->mapWithKeys(fn ($value) => [$value['name'] => $value['city'].' - '.$value['country']])
->toArray();
} catch (Exception) {
return [];
}
}
/**
* @throws ServerProviderError
* @throws ConnectionException
*/
public function create(): void
{
@ -106,6 +143,9 @@ public function create(): void
$this->server->save();
}
/**
* @throws ConnectionException
*/
public function isRunning(): bool
{
$status = Http::withToken($this->server->serverProvider->credentials['token'])
@ -118,6 +158,9 @@ public function isRunning(): bool
return $status->json()['server']['status'] == 'running';
}
/**
* @throws ConnectionException
*/
public function delete(): void
{
if (isset($this->server->provider_data['hetzner_id'])) {

View File

@ -15,9 +15,7 @@ class Linode extends AbstractProvider
public function createRules($input): array
{
$rules = [
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
];
$rules = [];
// plans
$plans = [];
foreach (config('serverproviders.linode.plans') as $plan) {
@ -69,14 +67,18 @@ public function connect(?array $credentials = null): bool
return true;
}
public function plans(): array
public function plans(?string $region): array
{
return config('serverproviders.linode.plans');
return collect(config('serverproviders.linode.plans'))
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
->toArray();
}
public function regions(): array
{
return config('serverproviders.linode.regions');
return collect(config('serverproviders.linode.regions'))
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
->toArray();
}
/**

View File

@ -14,7 +14,7 @@ public function data(array $input): array;
public function connect(?array $credentials = null): bool;
public function plans(): array;
public function plans(?string $region): array;
public function regions(): array;

View File

@ -17,9 +17,7 @@ class Vultr extends AbstractProvider
public function createRules($input): array
{
$rules = [
'os' => 'required|in:'.implode(',', config('core.operating_systems')),
];
$rules = [];
// plans
$plans = [];
foreach (config('serverproviders.vultr.plans') as $plan) {
@ -71,14 +69,18 @@ public function connect(?array $credentials = null): bool
return true;
}
public function plans(): array
public function plans(?string $region): array
{
return config('serverproviders.vultr.plans');
return collect(config('serverproviders.vultr.plans'))
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
->toArray();
}
public function regions(): array
{
return config('serverproviders.vultr.regions');
return collect(config('serverproviders.vultr.regions'))
->mapWithKeys(fn ($value) => [$value['value'] => $value['title']])
->toArray();
}
/**

View File

@ -27,7 +27,7 @@ function date_with_timezone($date, $timezone): string
function htmx(): HtmxResponse
{
return new HtmxResponse();
return new HtmxResponse;
}
function vito_version(): string
@ -48,3 +48,10 @@ function convert_time_format($string): string
return preg_replace('/(\d+)h/', '$1 hours', $string);
}
function get_public_key_content(): string
{
return str(file_get_contents(storage_path(config('core.ssh_public_key_name'))))
->replace("\n", '')
->toString();
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Traits;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
trait HasProjectThroughServer
{
public function project(): HasOneThrough
{
return $this->hasOneThrough(
Project::class,
Server::class,
'id',
'id',
'server_id',
'project_id'
);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Traits;
use Carbon\Carbon;
use Exception;
/**
* @property string $created_at_by_timezone
* @property string $updated_at_by_timezone
*/
trait HasTimezoneTimestamps
{
public function getCreatedAtByTimezoneAttribute(): string
{
return $this->getDateTimeByTimezone($this->created_at);
}
public function getUpdatedAtByTimezoneAttribute(): string
{
return $this->getDateTimeByTimezone($this->updated_at);
}
public function getDateTimeByTimezone(?Carbon $value = null): ?string
{
if ($value && auth()->user() && auth()->user()->timezone) {
try {
return date_with_timezone($value, auth()->user()->timezone);
} catch (Exception) {
}
}
return $value;
}
}

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;
}