Files
panel-pelican-dev/app/Filament/App/Resources/Servers/Pages/ListServers.php
2026-04-20 17:23:41 +02:00

282 lines
12 KiB
PHP

<?php
namespace App\Filament\App\Resources\Servers\Pages;
use App\Enums\CustomizationKey;
use App\Enums\ServerResourceType;
use App\Enums\SubuserPermission;
use App\Enums\TablerIcon;
use App\Filament\App\Resources\Servers\ServerResource;
use App\Filament\Components\Tables\Columns\ProgressBarColumn;
use App\Filament\Components\Tables\Columns\ServerEntryColumn;
use App\Filament\Server\Pages\Console;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\Column;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException;
use Livewire\Attributes\On;
class ListServers extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
public const WARNING_THRESHOLD = 0.7;
public const DANGER_THRESHOLD = 0.9;
protected static string $resource = ServerResource::class;
private DaemonServerRepository $daemonServerRepository;
public function boot(): void
{
$this->daemonServerRepository = new DaemonServerRepository();
}
/** @return Stack[]
*/
protected function gridColumns(): array
{
return [
Stack::make([
ServerEntryColumn::make('server_entry')
->warningThresholdPercent(static::WARNING_THRESHOLD)
->dangerThresholdPercent(static::DANGER_THRESHOLD)
->searchable(['name']),
]),
];
}
/** @return Column[] */
protected function tableColumns(): array
{
return [
ImageColumn::make('icon')
->label('')
->imageSize(46)
->state(fn (Server $server) => $server->icon ?: $server->egg->icon),
TextColumn::make('condition')
->label(trans('server/dashboard.status'))
->badge()
->tooltip(fn (Server $server) => $server->formatResource(ServerResourceType::Uptime, 2))
->icon(fn (Server $server) => $server->condition->getIcon())
->color(fn (Server $server) => $server->condition->getColor()),
TextColumn::make('name')
->label(trans('server/dashboard.title'))
->description(fn (Server $server) => $server->description)
->grow()
->searchable()
->sortable(),
TextColumn::make('allocation.address')
->label('')
->badge()
->visibleFrom('md')
->copyable()
->state(fn (Server $server) => $server->allocation->address ?? 'None'),
ProgressBarColumn::make('cpuUsage')
->label('')
->warningThresholdPercent(static::WARNING_THRESHOLD)
->dangerThresholdPercent(static::DANGER_THRESHOLD)
->maxValue(fn (Server $server) => ServerResourceType::CPULimit->getResourceAmount($server) === 0 ? (($server->node->systemInformation()['cpu_count'] ?? 0) * 100) : ServerResourceType::CPULimit->getResourceAmount($server))
->state(fn (Server $server) => $server->retrieveResources()['cpu_absolute'] ?? 0)
->helperLabel(fn (Server $server) => $server->formatResource(ServerResourceType::CPU, 0) . ' / ' . $server->formatResource(ServerResourceType::CPULimit, 0)),
ProgressBarColumn::make('memoryUsage')
->label('')
->warningThresholdPercent(static::WARNING_THRESHOLD)
->dangerThresholdPercent(static::DANGER_THRESHOLD)
->maxValue(fn (Server $server) => ServerResourceType::MemoryLimit->getResourceAmount($server) === 0 ? $server->node->statistics()['memory_total'] : ServerResourceType::MemoryLimit->getResourceAmount($server))
->state(fn (Server $server) => $server->retrieveResources()['memory_bytes'] ?? 0)
->helperLabel(fn (Server $server) => $server->formatResource(ServerResourceType::Memory) . ' / ' . $server->formatResource(ServerResourceType::MemoryLimit)),
ProgressBarColumn::make('diskUsage')
->label('')
->warningThresholdPercent(static::WARNING_THRESHOLD)
->dangerThresholdPercent(static::DANGER_THRESHOLD)
->maxValue(fn (Server $server) => ServerResourceType::DiskLimit->getResourceAmount($server) === 0 ? $server->node->statistics()['disk_total'] : ServerResourceType::DiskLimit->getResourceAmount($server))
->state(fn (Server $server) => $server->retrieveResources()['disk_bytes'] ?? 0)
->helperLabel(fn (Server $server) => $server->formatResource(ServerResourceType::Disk) . ' / ' . $server->formatResource(ServerResourceType::DiskLimit)),
];
}
public function table(Table $table): Table
{
$baseQuery = user()?->accessibleServers();
$usingGrid = user()?->getCustomization(CustomizationKey::DashboardLayout) === 'grid';
return $table
->paginated($usingGrid ? [10, 20, 30, 40] : [10, 20, 50, 100])
->defaultPaginationPageOption($usingGrid ? 10 : 20)
->query(fn () => $baseQuery)
->poll('15s')
->columns($usingGrid ? $this->gridColumns() : $this->tableColumns())
->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null)
->recordActions(!$usingGrid ? static::getPowerActionGroup() : [])
->recordActionsAlignment(Alignment::Center->value)
->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null)
->emptyStateIcon(TablerIcon::BrandDocker)
->emptyStateDescription('')
->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!')
->persistFiltersInSession()
->filters([
SelectFilter::make('egg')
->relationship('egg', 'name', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('egg_id')))
->searchable()
->preload(),
SelectFilter::make('owner')
->relationship('user', 'username', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('owner_id')))
->searchable()
->hidden(fn () => $this->activeTab === 'my')
->preload(),
]);
}
public function updatedActiveTab(): void
{
$this->resetTable();
}
public function getTabs(): array
{
$all = user()?->accessibleServers();
$my = (clone $all)->where('owner_id', user()?->id);
$other = (clone $all)->whereNot('owner_id', user()?->id);
return [
'my' => Tab::make('my')
->label(trans('server/dashboard.tabs.my'))
->badge(fn () => $my->count())
->modifyQueryUsing(fn () => $my),
'other' => Tab::make('other')
->label(trans('server/dashboard.tabs.other'))
->badge(fn () => $other->count())
->modifyQueryUsing(fn () => $other),
'all' => Tab::make('all')
->label(trans('server/dashboard.tabs.all'))
->badge($all->count()),
];
}
protected function getResourceColor(Server $server, string $resource): ?string
{
$current = null;
$limit = null;
switch ($resource) {
case 'cpu':
$current = $server->retrieveResources()['cpu_absolute'] ?? 0;
$limit = $server->cpu;
if ($server->cpu === 0) {
return null;
}
break;
case 'memory':
$current = $server->retrieveResources()['memory_bytes'] ?? 0;
$limit = $server->memory * 2 ** 20;
if ($server->memory === 0) {
return null;
}
break;
case 'disk':
$current = $server->retrieveResources()['disk_bytes'] ?? 0;
$limit = $server->disk * 2 ** 20;
if ($server->disk === 0) {
return null;
}
break;
default:
return null;
}
if ($current >= $limit * self::DANGER_THRESHOLD) {
return 'danger';
}
if ($current >= $limit * self::WARNING_THRESHOLD) {
return 'warning';
}
return null;
}
#[On('powerAction')]
public function powerAction(Server $server, string $action): void
{
try {
$this->daemonServerRepository->setServer($server)->power($action);
Notification::make()
->title(trans('server/dashboard.power_actions'))
->body(trans('server/dashboard.power_action_sent', ['action' => $action, 'name' => $server->name]))
->success()
->send();
cache()->forget("servers.$server->uuid.status");
$this->redirect(self::getUrl(['tab' => $this->activeTab]));
} catch (ConnectionException) {
Notification::make()
->title(trans('exceptions.node.error_connecting', ['node' => $server->node->name]))
->danger()
->send();
}
}
public static function getPowerActionGroup(): ActionGroup
{
return ActionGroup::make([
Action::make('start')
->label(trans('server/console.power_actions.start'))
->color('primary')
->icon(TablerIcon::PlayerPlayFilled)
->authorize(fn (Server $server) => user()?->can(SubuserPermission::ControlStart, $server))
->visible(fn (Server $server) => $server->retrieveStatus()->isStartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']),
Action::make('restart')
->label(trans('server/console.power_actions.restart'))
->color('gray')
->icon(TablerIcon::Reload)
->authorize(fn (Server $server) => user()?->can(SubuserPermission::ControlRestart, $server))
->visible(fn (Server $server) => $server->retrieveStatus()->isRestartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']),
Action::make('stop')
->label(trans('server/console.power_actions.stop'))
->color('danger')
->icon(TablerIcon::PlayerStopFilled)
->authorize(fn (Server $server) => user()?->can(SubuserPermission::ControlStop, $server))
->visible(fn (Server $server) => $server->retrieveStatus()->isStoppable() && !$server->retrieveStatus()->isKillable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']),
Action::make('kill')
->label(trans('server/console.power_actions.kill'))
->color('danger')
->icon(TablerIcon::AlertSquare)
->tooltip(trans('server/console.power_actions.kill_tooltip'))
->authorize(fn (Server $server) => user()?->can(SubuserPermission::ControlStop, $server))
->visible(fn (Server $server) => $server->retrieveStatus()->isKillable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']),
])
->icon(TablerIcon::Power)
->color('primary')
->tooltip(trans('server/dashboard.power_actions'))
->hidden(fn (Server $server) => $server->isInConflictState())
->iconSize(IconSize::Large);
}
}