2.x - php

This commit is contained in:
Saeed Vaziry 2024-09-28 15:19:55 +02:00
parent f6bc04763b
commit 32993025de
21 changed files with 698 additions and 83 deletions

View File

@ -5,16 +5,12 @@
use App\Enums\ServiceStatus; use App\Enums\ServiceStatus;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class InstallNewPHP class InstallNewPHP
{ {
public function install(Server $server, array $input): void public function install(Server $server, array $input): void
{ {
$this->validate($server, $input);
$php = new Service([ $php = new Service([
'server_id' => $server->id, 'server_id' => $server->id,
'type' => 'php', 'type' => 'php',
@ -38,22 +34,14 @@ public function install(Server $server, array $input): void
})->onConnection('ssh'); })->onConnection('ssh');
} }
/** public static function rules(Server $server): array
* @throws ValidationException
*/
private function validate(Server $server, array $input): void
{ {
Validator::make($input, [ return [
'version' => [ 'version' => [
'required', 'required',
Rule::in(config('core.php_versions')), Rule::in(config('core.php_versions')),
Rule::notIn($server->installedPHPVersions()),
], ],
])->validate(); ];
if (in_array($input['version'], $server->installedPHPVersions())) {
throw ValidationException::withMessages(
['version' => __('This version is already installed')]
);
}
} }
} }

View File

@ -2,10 +2,10 @@
namespace App\Actions\PHP; namespace App\Actions\PHP;
use App\Exceptions\SSHCommandError;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\SSH\Services\PHP\PHP; use App\SSH\Services\PHP\PHP;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -13,54 +13,53 @@ class InstallPHPExtension
{ {
public function install(Server $server, array $input): Service public function install(Server $server, array $input): Service
{ {
$this->validate($server, $input);
/** @var Service $service */ /** @var Service $service */
$service = $server->php($input['version']); $service = $server->php($input['version']);
if (in_array($input['extension'], $service->type_data['extensions'] ?? [])) {
throw ValidationException::withMessages([
'extension' => 'The extension is already installed.',
]);
}
$typeData = $service->type_data; $typeData = $service->type_data;
$typeData['extensions'] = $typeData['extensions'] ?? []; $typeData['extensions'] = $typeData['extensions'] ?? [];
$typeData['extensions'][] = $input['extension']; $typeData['extensions'][] = $input['extension'];
$service->type_data = $typeData; $service->type_data = $typeData;
$service->save(); $service->save();
dispatch(function () use ($service, $input) { dispatch(
/** @var PHP $handler */ /**
$handler = $service->handler(); * @throws SSHCommandError
$handler->installExtension($input['extension']); */
})->catch(function () use ($service, $input) { function () use ($service, $input) {
$service->refresh(); /** @var PHP $handler */
$typeData = $service->type_data; $handler = $service->handler();
$typeData['extensions'] = array_values(array_diff($typeData['extensions'], [$input['extension']])); $handler->installExtension($input['extension']);
$service->type_data = $typeData; })->catch(function () use ($service, $input) {
$service->save(); $service->refresh();
})->onConnection('ssh'); $typeData = $service->type_data;
$typeData['extensions'] = array_values(array_diff($typeData['extensions'], [$input['extension']]));
$service->type_data = $typeData;
$service->save();
})->onConnection('ssh');
return $service; return $service;
} }
/** public static function rules(Server $server): array
* @throws ValidationException
*/
private function validate(Server $server, array $input): void
{ {
Validator::make($input, [ return [
'extension' => [ 'extension' => [
'required', 'required',
'in:'.implode(',', config('core.php_extensions')), Rule::in(config('core.php_extensions')),
], ],
'version' => [ 'version' => [
'required', 'required',
Rule::in($server->installedPHPVersions()), Rule::exists('services', 'version')
->where('server_id', $server->id)
->where('type', 'php'),
], ],
])->validate(); ];
/** @var Service $service */
$service = $server->php($input['version']);
if (in_array($input['extension'], $service->type_data['extensions'])) {
throw ValidationException::withMessages(
['extension' => __('This extension already installed')]
)->errorBag('installPHPExtension');
}
} }
} }

View File

@ -6,7 +6,6 @@
use App\Models\Server; use App\Models\Server;
use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@ -19,8 +18,6 @@ class UpdatePHPIni
*/ */
public function update(Server $server, array $input): void public function update(Server $server, array $input): void
{ {
$this->validate($server, $input);
$service = $server->php($input['version']); $service = $server->php($input['version']);
$tmpName = Str::random(10).strtotime('now'); $tmpName = Str::random(10).strtotime('now');
@ -51,24 +48,23 @@ private function deleteTempFile(string $name): void
} }
} }
public function validate(Server $server, array $input): void public static function rules(Server $server): array
{ {
Validator::make($input, [ return [
'ini' => [ 'ini' => [
'required', 'required',
'string', 'string',
], ],
'version' => 'required|string', 'version' => [
'required',
Rule::exists('services', 'version')
->where('server_id', $server->id)
->where('type', 'php'),
],
'type' => [ 'type' => [
'required', 'required',
Rule::in([PHPIniType::CLI, PHPIniType::FPM]), Rule::in([PHPIniType::CLI, PHPIniType::FPM]),
], ],
])->validate(); ];
if (! in_array($input['version'], $server->installedPHPVersions())) {
throw ValidationException::withMessages(
['version' => __('This version is not installed')]
);
}
} }
} }

View File

@ -2,11 +2,13 @@
namespace App\Models; namespace App\Models;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
/** /**
* @property int $server_id * @property int $server_id
@ -46,7 +48,7 @@ public static function boot(): void
if (Storage::disk($log->disk)->exists($log->name)) { if (Storage::disk($log->disk)->exists($log->name)) {
Storage::disk($log->disk)->delete($log->name); Storage::disk($log->disk)->delete($log->name);
} }
} catch (\Exception $e) { } catch (Exception $e) {
Log::error($e->getMessage(), ['exception' => $e]); Log::error($e->getMessage(), ['exception' => $e]);
} }
}); });
@ -67,6 +69,11 @@ public function site(): BelongsTo
return $this->belongsTo(Site::class); return $this->belongsTo(Site::class);
} }
public function download(): StreamedResponse
{
return Storage::disk($this->disk)->download($this->name);
}
public static function getRemote($query, bool $active = true, ?Site $site = null) public static function getRemote($query, bool $active = true, ?Site $site = null)
{ {
$query->where('is_remote', $active); $query->where('is_remote', $active);

View File

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use App\Actions\Service\Manage; use App\Actions\Service\Manage;
use App\Enums\ServiceStatus;
use App\Exceptions\ServiceInstallationFailed; use App\Exceptions\ServiceInstallationFailed;
use App\SSH\Services\ServiceInterface; use App\SSH\Services\ServiceInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -54,6 +55,21 @@ public static function boot(): void
}); });
} }
public static array $statusColors = [
ServiceStatus::READY => 'success',
ServiceStatus::INSTALLING => 'warning',
ServiceStatus::INSTALLATION_FAILED => 'danger',
ServiceStatus::UNINSTALLING => 'warning',
ServiceStatus::FAILED => 'danger',
ServiceStatus::STARTING => 'warning',
ServiceStatus::STOPPING => 'warning',
ServiceStatus::RESTARTING => 'warning',
ServiceStatus::STOPPED => 'danger',
ServiceStatus::ENABLING => 'warning',
ServiceStatus::DISABLING => 'warning',
ServiceStatus::DISABLED => 'gray',
];
public function server(): BelongsTo public function server(): BelongsTo
{ {
return $this->belongsTo(Server::class); return $this->belongsTo(Server::class);
@ -72,7 +88,7 @@ public function handler(): ServiceInterface
public function validateInstall($result): void public function validateInstall($result): void
{ {
if (! Str::contains($result, 'Active: active')) { if (! Str::contains($result, 'Active: active')) {
throw new ServiceInstallationFailed(); throw new ServiceInstallationFailed;
} }
} }

View File

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

View File

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

View File

@ -41,7 +41,7 @@ public function boot(): void
); );
FilamentColor::register([ FilamentColor::register([
'slate' => Color::Slate, 'slate' => Color::Slate,
'gray' => Color::Zinc, 'gray' => Color::Gray,
'red' => Color::Red, 'red' => Color::Red,
'orange' => Color::Orange, 'orange' => Color::Orange,
'amber' => Color::Amber, 'amber' => Color::Amber,

View File

@ -24,6 +24,11 @@ public static function getNavigationItemActiveRoutePattern(): string
return static::getRouteName().'*'; return static::getRouteName().'*';
} }
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', Server::class) ?? false;
}
public function getWidgets(): array public function getWidgets(): array
{ {
return [ return [
@ -36,6 +41,7 @@ protected function getHeaderActions(): array
return [ return [
Action::make('create') Action::make('create')
->label('Create a Server') ->label('Create a Server')
->icon('heroicon-o-plus')
->url(Create::getUrl()) ->url(Create::getUrl())
->authorize('create', Server::class), ->authorize('create', Server::class),
]; ];

View File

@ -0,0 +1,41 @@
<?php
namespace App\Web\Pages\Servers\Logs;
use App\Models\Server;
use App\Models\ServerLog;
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
use App\Web\Traits\PageHasServer;
use App\Web\Traits\PageHasWidgets;
use Filament\Pages\Page;
class Index extends Page
{
use PageHasServer;
use PageHasWidgets;
protected static ?string $slug = 'servers/{server}/logs';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Logs';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [ServerLog::class, static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array
{
return [
[LogsList::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
return [];
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Web\Pages\Servers\Logs\Widgets;
use App\Models\Server;
use App\Models\ServerLog;
use Exception;
use Filament\Forms\Components\DatePicker;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\View\ComponentAttributeBag;
class LogsList extends Widget
{
public Server $server;
protected function getTableQuery(): Builder
{
return ServerLog::query()->where('server_id', $this->server->id);
}
protected static ?string $heading = 'Logs';
protected function getTableColumns(): array
{
return [
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('created_at_by_timezone')
->label('Created At')
->sortable(),
];
}
protected function applyDefaultSortingToTableQuery(Builder $query): Builder
{
return $query->latest('created_at');
}
/**
* @throws Exception
*/
public function getTable(): Table
{
return $this->table
->filters([
Filter::make('created_at')
->form([
DatePicker::make('created_from'),
DatePicker::make('created_until'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['created_from'],
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '>=', $date),
)
->when(
$data['created_until'],
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date),
);
}),
])
->actions([
Action::make('view')
->label('View')
->icon('heroicon-o-eye')
->authorize(fn ($record) => auth()->user()->can('view', $record))
->modalHeading('View Log')
->modalContent(function (ServerLog $record) {
return view('components.console-view', [
'slot' => $record->getContent(),
'attributes' => new ComponentAttributeBag,
]);
})
->modalSubmitAction(false)
->modalCancelActionLabel('Close'),
Action::make('download')
->label('Download')
->color('secondary')
->icon('heroicon-o-archive-box-arrow-down')
->authorize(fn ($record) => auth()->user()->can('view', $record))
->action(fn (ServerLog $record) => $record->download()),
]);
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Web\Pages\Servers\PHP;
use App\Actions\PHP\InstallNewPHP;
use App\Models\Server;
use App\Models\Service;
use App\Web\Pages\Servers\PHP\Widgets\PHPList;
use App\Web\Traits\PageHasServer;
use App\Web\Traits\PageHasWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Pages\Page;
class Index extends Page
{
use PageHasServer;
use PageHasWidgets;
protected static ?string $slug = 'servers/{server}/php';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'PHP';
protected function getExtraAttributes(): array
{
return [];
}
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Service::class, static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array
{
return [
[PHPList::class, ['server' => $this->server]],
];
}
protected function getHeaderActions(): array
{
$phps = [];
foreach (config('core.php_versions') as $version) {
if (! $this->server->service('php', $version) && $version !== 'none') {
$phps[] = Action::make($version)
->label($version)
->requiresConfirmation()
->modalHeading('Install PHP '.$version)
->modalSubmitActionLabel('Install')
->action(function () use ($version) {
app(InstallNewPHP::class)->install($this->server, ['version' => $version]);
$this->dispatch('$refresh');
});
}
}
return [
ActionGroup::make($phps)
->authorize(fn () => auth()->user()?->can('create', [Service::class, $this->server]))
->label('Install PHP')
->icon('heroicon-o-plus')
->dropdownPlacement('bottom-end')
->color('primary')
->button(),
];
}
}

View File

@ -0,0 +1,231 @@
<?php
namespace App\Web\Pages\Servers\PHP\Widgets;
use App\Actions\PHP\ChangeDefaultCli;
use App\Actions\PHP\GetPHPIni;
use App\Actions\PHP\InstallPHPExtension;
use App\Actions\PHP\UpdatePHPIni;
use App\Actions\Service\Manage;
use App\Actions\Service\Uninstall;
use App\Enums\PHPIniType;
use App\Models\Server;
use App\Models\Service;
use Exception;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as Widget;
use Illuminate\Database\Eloquent\Builder;
class PHPList extends Widget
{
public Server $server;
protected function getTableQuery(): Builder
{
return Service::query()->where('type', 'php')->where('server_id', $this->server->id);
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
TextColumn::make('version')
->sortable(),
TextColumn::make('status')
->label('Status')
->badge()
->color(fn (Service $service) => Service::$statusColors[$service->status])
->sortable(),
TextColumn::make('is_default')
->label('Default Cli')
->badge()
->color(fn (Service $service) => $service->is_default ? 'primary' : 'gray')
->state(fn (Service $service) => $service->is_default ? 'Yes' : 'No')
->sortable(),
TextColumn::make('created_at_by_timezone')
->label('Installed At'),
];
}
/**
* @throws Exception
*/
public function getTable(): Table
{
return $this->table
->actions([
ActionGroup::make([
$this->installExtensionAction(),
$this->editPHPIniAction(PHPIniType::CLI),
$this->editPHPIniAction(PHPIniType::FPM),
$this->defaultPHPCliAction(),
$this->restartFPMAction(),
$this->uninstallAction(),
]),
]);
}
private function installExtensionAction(): Action
{
return Action::make('install-extension')
->authorize(fn (Service $php) => auth()->user()?->can('update', [$php, $this->server]))
->label('Install Extension')
->modalHeading('Install PHP Extension')
->modalWidth(MaxWidth::Large)
->modalSubmitActionLabel('Install')
->form([
Hidden::make('version')
->default(fn (Service $service) => $service->version)
->rules(InstallPHPExtension::rules($this->server)['version']),
Select::make('extension')
->options(
collect(config('core.php_extensions'))
->mapWithKeys(fn ($extension) => [$extension => $extension])
)
->rules(InstallPHPExtension::rules($this->server)['extension']),
])
->action(function (array $data) {
$this->validate();
try {
app(InstallPHPExtension::class)->install($this->server, $data);
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
});
}
private function defaultPHPCliAction(): Action
{
return Action::make('default-php-cli')
->authorize(fn (Service $php) => auth()->user()?->can('update', [$php, $this->server]))
->label('Make Default CLI')
->hidden(fn (Service $service) => $service->is_default)
->action(function (Service $service) {
try {
app(ChangeDefaultCli::class)->change($this->server, ['version' => $service->version]);
Notification::make()
->success()
->title('Default PHP CLI changed!')
->send();
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
});
}
private function editPHPIniAction(string $type): Action
{
return Action::make('php-ini-'.$type)
->authorize(fn (Service $php) => auth()->user()?->can('update', [$php, $this->server]))
->label('Update PHP ini ('.$type.')')
->modalWidth(MaxWidth::TwoExtraLarge)
->modalSubmitActionLabel('Save')
->form([
Hidden::make('type')
->default($type)
->rules(UpdatePHPIni::rules($this->server)['type']),
Hidden::make('version')
->default(fn (Service $service) => $service->version)
->rules(UpdatePHPIni::rules($this->server)['version']),
Textarea::make('ini')
->label('PHP ini')
->rows(20)
->rules(UpdatePHPIni::rules($this->server)['ini'])
->default(fn (Service $service) => app(GetPHPIni::class)->getIni($this->server, [
'type' => $type,
'version' => $service->version,
])),
])
->action(function (array $data) {
$this->validate();
try {
app(UpdatePHPIni::class)->update($this->server, $data);
Notification::make()
->success()
->title('PHP ini updated!')
->body('Restarting PHP...')
->send();
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
});
}
private function restartFPMAction(): Action
{
return Action::make('restart-fpm')
->authorize(fn (Service $php) => auth()->user()?->can('update', [$php, $this->server]))
->label('Restart PHP FPM')
->action(function (Service $service) {
try {
app(Manage::class)->restart($service);
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
});
}
private function uninstallAction(): Action
{
return Action::make('uninstall')
->authorize(fn (Service $php) => auth()->user()?->can('delete', [$php, $this->server]))
->label('Uninstall')
->color('danger')
->requiresConfirmation()
->action(function (Service $service) {
try {
app(Uninstall::class)->uninstall($service);
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
});
}
}

View File

@ -30,7 +30,7 @@ class Settings extends Page
public static function canAccess(): bool public static function canAccess(): bool
{ {
return auth()->user()?->can('update', request()->route('server')) ?? false; return auth()->user()?->can('update', static::getServerFromRoute()) ?? false;
} }
public function getWidgets(): array public function getWidgets(): array

View File

@ -23,6 +23,11 @@ class Index extends Page
public Server $server; public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Site::class, static::getServerFromRoute()]) ?? false;
}
public function getWidgets(): array public function getWidgets(): array
{ {
return [ return [
@ -36,7 +41,8 @@ protected function getHeaderActions(): array
CreateAction::make() CreateAction::make()
->authorize(fn () => auth()->user()?->can('create', [Site::class, $this->server])) ->authorize(fn () => auth()->user()?->can('create', [Site::class, $this->server]))
->createAnother(false) ->createAnother(false)
->label('Create a Site'), ->label('Create a Site')
->icon('heroicon-o-plus'),
]; ];
} }
} }

View File

@ -3,7 +3,10 @@
namespace App\Web\Pages\Servers; namespace App\Web\Pages\Servers;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerLog;
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
use App\Web\Pages\Servers\Widgets\Installing; use App\Web\Pages\Servers\Widgets\Installing;
use App\Web\Pages\Servers\Widgets\ServerStats;
use App\Web\Traits\PageHasServer; use App\Web\Traits\PageHasServer;
use App\Web\Traits\PageHasWidgets; use App\Web\Traits\PageHasWidgets;
use Filament\Pages\Page; use Filament\Pages\Page;
@ -31,7 +34,7 @@ public function mount(): void
public static function canAccess(): bool public static function canAccess(): bool
{ {
return auth()->user()?->can('view', request()->route('server')) ?? false; return auth()->user()?->can('view', static::getServerFromRoute()) ?? false;
} }
#[On('$refresh')] #[On('$refresh')]
@ -48,12 +51,18 @@ public function refresh(): void
public function getWidgets(): array public function getWidgets(): array
{ {
$widgets = [];
if ($this->server->isInstalling()) { if ($this->server->isInstalling()) {
return [ $widgets[] = [Installing::class, ['server' => $this->server]];
[Installing::class, ['server' => $this->server]], } else {
]; $widgets[] = [ServerStats::class, ['server' => $this->server]];
} }
return []; if (auth()->user()->can('viewAny', [ServerLog::class, $this->server])) {
$widgets[] = [LogsList::class, ['server' => $this->server]];
}
return $widgets;
} }
} }

View File

@ -0,0 +1,36 @@
<?php
namespace App\Web\Pages\Servers\Widgets;
use App\Models\Server;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class ServerStats extends BaseWidget
{
public Server $server;
protected static bool $isLazy = false;
protected function getStats(): array
{
$stats = [];
if ($this->server->webserver()) {
$stats[] = Stat::make('Sites', $this->server->sites()->count())
->icon('heroicon-o-globe-alt');
}
if ($this->server->database()) {
$stats[] = Stat::make('Databases', $this->server->databases()->count())
->icon('heroicon-o-circle-stack');
}
if ($this->server->defaultService('php')) {
$stats[] = Stat::make('PHP Version', $this->server->defaultService('php')->version)
->icon('heroicon-o-command-line');
}
return $stats;
}
}

View File

@ -60,6 +60,7 @@ protected function getHeaderActions(): array
DeleteAction::make() DeleteAction::make()
->record($this->project) ->record($this->project)
->label('Delete Project') ->label('Delete Project')
->icon('heroicon-o-trash')
->modalHeading('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.') ->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) { ->using(function (Project $record) {

View File

@ -39,6 +39,7 @@ protected function getHeaderActions(): array
return [ return [
CreateAction::make() CreateAction::make()
->label('Connect') ->label('Connect')
->icon('heroicon-o-wifi')
->modalHeading('Connect to a Server Provider') ->modalHeading('Connect to a Server Provider')
->modalSubmitActionLabel('Connect') ->modalSubmitActionLabel('Connect')
->createAnother(false) ->createAnother(false)

View File

@ -43,6 +43,7 @@ protected function getHeaderActions(): array
return [ return [
CreateAction::make() CreateAction::make()
->label('Create User') ->label('Create User')
->icon('heroicon-o-plus')
->authorize('create', User::class) ->authorize('create', User::class)
->action(function (array $data) { ->action(function (array $data) {
$user = app(CreateUser::class)->create($data); $user = app(CreateUser::class)->create($data);

View File

@ -2,13 +2,17 @@
namespace App\Web\Traits; namespace App\Web\Traits;
use App\Models\Site; use App\Models\Server;
use App\Web\Pages\Servers\Settings; use App\Web\Pages\Servers\Logs\Index as LogsIndex;
use App\Web\Pages\Servers\Sites\Index; use App\Web\Pages\Servers\PHP\Index as PHPIndex;
use App\Web\Pages\Servers\View; use App\Web\Pages\Servers\Settings as ServerSettings;
use App\Web\Pages\Servers\Sites\Index as SitesIndex;
use App\Web\Pages\Servers\View as ServerView;
use App\Web\Pages\Servers\Widgets\ServerSummary; use App\Web\Pages\Servers\Widgets\ServerSummary;
use Filament\Navigation\NavigationItem; use Filament\Navigation\NavigationItem;
use Illuminate\Contracts\Support\Htmlable; use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
trait PageHasServer trait PageHasServer
{ {
@ -21,25 +25,39 @@ public function getSubNavigation(): array
{ {
$items = []; $items = [];
if (auth()->user()?->can('view', $this->server)) { if (ServerView::canAccess()) {
$items[] = NavigationItem::make('Overview') $items[] = NavigationItem::make('Overview')
->icon('heroicon-o-chart-pie') ->icon('heroicon-o-chart-pie')
->isActiveWhen(fn () => request()->routeIs(View::getRouteName())) ->isActiveWhen(fn () => request()->routeIs(ServerView::getRouteName()))
->url(View::getUrl(parameters: ['server' => $this->server])); ->url(ServerView::getUrl(parameters: ['server' => $this->server]));
} }
if (auth()->user()?->can('viewAny', [Site::class, $this->server])) { if (SitesIndex::canAccess()) {
$items[] = NavigationItem::make('Sites') $items[] = NavigationItem::make('Sites')
->icon('heroicon-o-globe-alt') ->icon('heroicon-o-globe-alt')
->isActiveWhen(fn () => request()->routeIs(Index::getRouteName().'*')) ->isActiveWhen(fn () => request()->routeIs(SitesIndex::getRouteName().'*'))
->url(Index::getUrl(parameters: ['server' => $this->server])); ->url(SitesIndex::getUrl(parameters: ['server' => $this->server]));
} }
if (auth()->user()?->can('update', $this->server)) { if (PHPIndex::canAccess()) {
$items[] = NavigationItem::make('PHP')
->icon('heroicon-o-code-bracket')
->isActiveWhen(fn () => request()->routeIs(PHPIndex::getRouteName().'*'))
->url(PHPIndex::getUrl(parameters: ['server' => $this->server]));
}
if (LogsIndex::canAccess()) {
$items[] = NavigationItem::make('Logs')
->icon('heroicon-o-square-3-stack-3d')
->isActiveWhen(fn () => request()->routeIs(LogsIndex::getRouteName().'*'))
->url(LogsIndex::getUrl(parameters: ['server' => $this->server]));
}
if (ServerSettings::canAccess()) {
$items[] = NavigationItem::make('Settings') $items[] = NavigationItem::make('Settings')
->icon('heroicon-o-cog-6-tooth') ->icon('heroicon-o-cog-6-tooth')
->isActiveWhen(fn () => request()->routeIs(Settings::getRouteName().'*')) ->isActiveWhen(fn () => request()->routeIs(ServerSettings::getRouteName().'*'))
->url(Settings::getUrl(parameters: ['server' => $this->server])); ->url(ServerSettings::getUrl(parameters: ['server' => $this->server]));
} }
return $items; return $items;
@ -54,6 +72,25 @@ protected function getHeaderWidgets(): array
]; ];
} }
protected static function getServerFromRoute(): ?Server
{
$server = request()->route('server');
if (! $server) {
$server = Route::getRoutes()->match(Request::create(url()->previous()))->parameter('server');
}
if ($server instanceof Server) {
return $server;
}
if ($server) {
return Server::query()->find($server);
}
return null;
}
public function getHeaderWidgetsColumns(): int|string|array public function getHeaderWidgetsColumns(): int|string|array
{ {
return 1; return 1;