Cleanup & fix server list (#1433)

This commit is contained in:
Boy132 2025-06-12 08:54:00 +02:00 committed by GitHub
parent b05eabfdb0
commit 1dc5ec027e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 159 additions and 260 deletions

View File

@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function isStartable(): bool public function isStartable(): bool
{ {
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]); return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]);
} }
public function isRestartable(): bool public function isRestartable(): bool
@ -97,18 +97,16 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
return true; return true;
} }
return !in_array($this, [ContainerStatus::Offline]); return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
} }
public function isStoppable(): bool public function isStoppable(): bool
{ {
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]); return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]);
} }
public function isKillable(): bool public function isKillable(): bool
{ {
// [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created] return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]);
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
} }
} }

View File

@ -27,8 +27,16 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
}; };
} }
public function getColor(): string public function getColor(bool $hex = false): string
{ {
if ($hex) {
return match ($this) {
self::Normal, self::Installing, self::RestoringBackup => '#2563EB',
self::Suspended => '#D97706',
self::InstallFailed, self::ReinstallFailed => '#EF4444',
};
}
return match ($this) { return match ($this) {
self::Normal => 'primary', self::Normal => 'primary',
self::Installing => 'primary', self::Installing => 'primary',

View File

@ -9,12 +9,13 @@ use App\Filament\Server\Pages\Console;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use AymanAlhattami\FilamentContextMenu\Columns\ContextMenuTextColumn;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Components\Tab; use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\Alignment;
use Filament\Tables\Actions\Action; use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\ColumnGroup; use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Columns\Column;
use Filament\Tables\Columns\Layout\Stack; use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
@ -38,121 +39,73 @@ class ListServers extends ListRecords
$this->daemonPowerRepository = new DaemonPowerRepository(); $this->daemonPowerRepository = new DaemonPowerRepository();
} }
public function table(Table $table): Table /** @return Stack[] */
protected function gridColumns(): array
{ {
$baseQuery = auth()->user()->accessibleServers();
$menuOptions = function (Server $server) {
$status = $server->retrieveStatus();
return [ return [
Action::make('start') Stack::make([
->color('primary') ServerEntryColumn::make('server_entry')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server)) ->searchable(['name']),
->visible(fn () => $status->isStartable()) ]),
->dispatch('powerAction', ['server' => $server, 'action' => 'start'])
->icon('tabler-player-play-filled'),
Action::make('restart')
->color('gray')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->visible(fn () => $status->isRestartable())
->dispatch('powerAction', ['server' => $server, 'action' => 'restart'])
->icon('tabler-refresh'),
Action::make('stop')
->color('danger')
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn () => $status->isStoppable())
->dispatch('powerAction', ['server' => $server, 'action' => 'stop'])
->icon('tabler-player-stop-filled'),
Action::make('kill')
->color('danger')
->tooltip('This can result in data corruption and/or data loss!')
->dispatch('powerAction', ['server' => $server, 'action' => 'kill'])
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn () => $status->isKillable())
->icon('tabler-alert-square'),
]; ];
}; }
$viewOne = [ /** @return Column[] */
ContextMenuTextColumn::make('condition') protected function tableColumns(): array
->label('') {
->default('unknown') return [
->wrap() TextColumn::make('condition')
->label('Status')
->badge() ->badge()
->alignCenter()
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time)) ->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
->icon(fn (Server $server) => $server->condition->getIcon()) ->icon(fn (Server $server) => $server->condition->getIcon())
->color(fn (Server $server) => $server->condition->getColor()) ->color(fn (Server $server) => $server->condition->getColor()),
->contextMenuActions($menuOptions) TextColumn::make('name')
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()), ->label('Server')
]; ->description(fn (Server $server) => $server->description)
->grow()
$viewTwo = [ ->searchable(),
ContextMenuTextColumn::make('name') TextColumn::make('allocation.address')
->label('')
->size('md')
->searchable()
->contextMenuActions($menuOptions)
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
ContextMenuTextColumn::make('allocation.address')
->label('') ->label('')
->badge() ->badge()
->copyable(request()->isSecure()) ->visibleFrom('md')
->contextMenuActions($menuOptions) ->copyable(request()->isSecure()),
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
];
$viewThree = [
TextColumn::make('cpuUsage') TextColumn::make('cpuUsage')
->label('') ->label('Resources')
->icon('tabler-cpu') ->icon('tabler-cpu')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0)) ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0))
->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage)) ->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage))
->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')), ->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
TextColumn::make('memoryUsage') TextColumn::make('memoryUsage')
->label('') ->label('')
->icon('tabler-memory') ->icon('tabler-device-desktop-analytics')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true)) ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true))
->state(fn (Server $server) => $server->formatResource('memory_bytes')) ->state(fn (Server $server) => $server->formatResource('memory_bytes'))
->color(fn (Server $server) => $this->getResourceColor($server, 'memory')), ->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
TextColumn::make('diskUsage') TextColumn::make('diskUsage')
->label('') ->label('')
->icon('tabler-device-floppy') ->icon('tabler-device-sd-card')
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true)) ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true))
->state(fn (Server $server) => $server->formatResource('disk_bytes')) ->state(fn (Server $server) => $server->formatResource('disk_bytes'))
->color(fn (Server $server) => $this->getResourceColor($server, 'disk')), ->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
]; ];
}
public function table(Table $table): Table
{
$baseQuery = auth()->user()->accessibleServers();
$usingGrid = (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid';
return $table return $table
->paginated(false) ->paginated(false)
->query(fn () => $baseQuery) ->query(fn () => $baseQuery)
->poll('15s') ->poll('15s')
->columns( ->columns($usingGrid ? $this->gridColumns() : $this->tableColumns())
(auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid' ->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null)
? [ ->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions()) : [])
Stack::make([ ->actionsAlignment(Alignment::Center->value)
ServerEntryColumn::make('server_entry') ->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null)
->searchable(['name']),
]),
]
: [
ColumnGroup::make('Status')
->label('Status')
->columns($viewOne),
ColumnGroup::make('Server')
->label('Servers')
->columns($viewTwo),
ColumnGroup::make('Resources')
->label('Resources')
->columns($viewThree),
]
)
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
->contentGrid([
'default' => 1,
'md' => 2,
])
->emptyStateIcon('tabler-brand-docker') ->emptyStateIcon('tabler-brand-docker')
->emptyStateDescription('') ->emptyStateDescription('')
->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!') ->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!')
@ -195,36 +148,33 @@ class ListServers extends ListRecords
]; ];
} }
public function getResourceColor(Server $server, string $resource): ?string protected function getResourceColor(Server $server, string $resource): ?string
{ {
$current = null; $current = null;
$limit = null; $limit = null;
switch ($resource) { switch ($resource) {
case 'cpu': case 'cpu':
$current = $server->resources()['cpu_absolute'] ?? 0; $current = $server->retrieveResources()['cpu_absolute'] ?? 0;
$limit = $server->cpu; $limit = $server->cpu;
if ($server->cpu === 0) { if ($server->cpu === 0) {
return null; return null;
} }
break; break;
case 'memory': case 'memory':
$current = $server->resources()['memory_bytes'] ?? 0; $current = $server->retrieveResources()['memory_bytes'] ?? 0;
$limit = $server->memory * 2 ** 20; $limit = $server->memory * 2 ** 20;
if ($server->memory === 0) { if ($server->memory === 0) {
return null; return null;
} }
break; break;
case 'disk': case 'disk':
$current = $server->resources()['disk_bytes'] ?? 0; $current = $server->retrieveResources()['disk_bytes'] ?? 0;
$limit = $server->disk * 2 ** 20; $limit = $server->disk * 2 ** 20;
if ($server->disk === 0) { if ($server->disk === 0) {
return null; return null;
} }
break; break;
default: default:
return null; return null;
} }
@ -238,7 +188,6 @@ class ListServers extends ListRecords
} }
return null; return null;
} }
#[On('powerAction')] #[On('powerAction')]
@ -253,6 +202,8 @@ class ListServers extends ListRecords
->success() ->success()
->send(); ->send();
cache()->forget("servers.$server->uuid.status");
$this->redirect(self::getUrl(['activeTab' => $this->activeTab])); $this->redirect(self::getUrl(['activeTab' => $this->activeTab]));
} catch (ConnectionException) { } catch (ConnectionException) {
Notification::make() Notification::make()
@ -261,4 +212,36 @@ class ListServers extends ListRecords
->send(); ->send();
} }
} }
/** @return Action[] */
public static function getPowerActions(): array
{
return [
Action::make('start')
->color('primary')
->icon('tabler-player-play-filled')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']),
Action::make('restart')
->color('gray')
->icon('tabler-reload')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isRestartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']),
Action::make('stop')
->color('danger')
->icon('tabler-player-stop-filled')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStoppable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']),
Action::make('kill')
->color('danger')
->icon('tabler-alert-square')
->tooltip('This can result in data corruption and/or data loss!')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isKillable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']),
];
}
} }

View File

@ -333,26 +333,6 @@ class Node extends Model implements Validatable
}); });
} }
/**
* @return array<array-key, mixed>
*/
public function serverStatuses(): array
{
$statuses = [];
try {
$statuses = Http::daemon($this)->connectTimeout(1)->timeout(1)->get('/api/servers')->json() ?? [];
} catch (Exception $exception) {
report($exception);
}
foreach ($statuses as $status) {
$uuid = fluent($status)->get('configuration.uuid');
cache()->remember("servers.$uuid.container.status", now()->addMinute(), fn () => fluent($status)->get('state'));
}
return $statuses;
}
/** @return array{ /** @return array{
* memory_total: int, memory_used: int, * memory_total: int, memory_used: int,
* swap_total: int, swap_used: int, * swap_total: int, swap_used: int,

View File

@ -435,25 +435,24 @@ class Server extends Model implements Validatable
public function retrieveStatus(): ContainerStatus public function retrieveStatus(): ContainerStatus
{ {
$status = cache()->get("servers.$this->uuid.container.status"); return cache()->remember("servers.$this->uuid.status", now()->addSeconds(15), function () {
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
$details = app(DaemonServerRepository::class)->setServer($this)->getDetails();
if ($status === null) { return ContainerStatus::tryFrom(Arr::get($details, 'state')) ?? ContainerStatus::Missing;
$this->node->serverStatuses(); });
$status = cache()->get("servers.$this->uuid.container.status");
}
return ContainerStatus::tryFrom($status) ?? ContainerStatus::Missing;
} }
/** /**
* @return array<mixed> * @return array<mixed>
*/ */
public function resources(): array public function retrieveResources(): array
{ {
return cache()->remember("resources:$this->uuid", now()->addSeconds(15), function () { return cache()->remember("servers.$this->uuid.resources", now()->addSeconds(15), function () {
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
return Arr::get(app(DaemonServerRepository::class)->setServer($this)->getDetails(), 'utilization', []); $details = app(DaemonServerRepository::class)->setServer($this)->getDetails();
return Arr::get($details, 'utilization', []);
}); });
} }
@ -461,7 +460,7 @@ class Server extends Model implements Validatable
{ {
$resourceAmount = $this->{$resourceKey} ?? 0; $resourceAmount = $this->{$resourceKey} ?? 0;
if (!$limit) { if (!$limit) {
$resourceAmount = $this->resources()[$resourceKey] ?? 0; $resourceAmount = $this->retrieveResources()[$resourceKey] ?? 0;
} }
if ($type === ServerResourceType::Time) { if ($type === ServerResourceType::Time) {
@ -494,7 +493,7 @@ class Server extends Model implements Validatable
public function condition(): Attribute public function condition(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => $this->isSuspended() ? ServerState::Suspended : $this->status ?? $this->retrieveStatus(), get: fn () => $this->status ?? $this->retrieveStatus(),
); );
} }
} }

View File

@ -11,7 +11,6 @@
"ext-zip": "*", "ext-zip": "*",
"abdelhamiderrahmouni/filament-monaco-editor": "^0.2.5", "abdelhamiderrahmouni/filament-monaco-editor": "^0.2.5",
"aws/aws-sdk-php": "^3.344", "aws/aws-sdk-php": "^3.344",
"aymanalhattami/filament-context-menu": "^1.0",
"calebporzio/sushi": "^2.5", "calebporzio/sushi": "^2.5",
"chillerlan/php-qrcode": "^5.0.2", "chillerlan/php-qrcode": "^5.0.2",
"dedoc/scramble": "^0.12.10", "dedoc/scramble": "^0.12.10",

82
composer.lock generated
View File

@ -1113,83 +1113,7 @@
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.344.3" "source": "https://github.com/aws/aws-sdk-php/tree/3.344.3"
}, },
"time": "2025-06-09T18:04:12+00:00" "time": "2025-06-02T18:04:47+00:00"
},
{
"name": "aymanalhattami/filament-context-menu",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/aymanalhattami/filament-context-menu.git",
"reference": "5118d36303e86891d3037e6e26882d548b880b9c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aymanalhattami/filament-context-menu/zipball/5118d36303e86891d3037e6e26882d548b880b9c",
"reference": "5118d36303e86891d3037e6e26882d548b880b9c",
"shasum": ""
},
"require": {
"filament/filament": "^3.0",
"php": "^8.2",
"spatie/laravel-package-tools": "^1.15.0"
},
"require-dev": {
"larastan/larastan": "^2.0",
"laravel/pint": "^1.0",
"nunomaduro/collision": "^8.0",
"orchestra/testbench": "^9.0",
"pestphp/pest": "^3.0",
"pestphp/pest-plugin-arch": "^3.0",
"pestphp/pest-plugin-laravel": "^3.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Skeleton": "AymanAlhattami\\FilamentContextMenu\\Facades\\FilamentContextMenu"
},
"providers": [
"AymanAlhattami\\FilamentContextMenu\\FilamentContextMenuServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"AymanAlhattami\\FilamentContextMenu\\": "src/",
"AymanAlhattami\\FilamentContextMenu\\Database\\Factories\\": "database/factories/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ayman Alhattami",
"email": "ayman.m.alhattami@gmail.com",
"role": "Developer"
}
],
"description": "context menu (right click menu) for filament",
"homepage": "https://github.com/aymanalhattami/filament-context-menu",
"keywords": [
"ayman alhattami",
"context menu",
"filament",
"filament admin panel",
"filament context menu",
"filament_context_menu",
"laravel"
],
"support": {
"issues": "https://github.com/aymanalhattami/filament-context-menu/issues",
"source": "https://github.com/aymanalhattami/filament-context-menu"
},
"time": "2024-09-22T10:47:31+00:00"
}, },
{ {
"name": "blade-ui-kit/blade-heroicons", "name": "blade-ui-kit/blade-heroicons",
@ -15981,7 +15905,7 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": {}, "stability-flags": [],
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
@ -15992,7 +15916,7 @@
"ext-pdo": "*", "ext-pdo": "*",
"ext-zip": "*" "ext-zip": "*"
}, },
"platform-dev": {}, "platform-dev": [],
"platform-overrides": { "platform-overrides": {
"php": "8.2" "php": "8.2"
}, },

View File

@ -1,4 +1,5 @@
<div class="relative"> <div wire:poll.15s class="relative">
<a href="{{ \App\Filament\Server\Pages\Console::getUrl(panel: 'server', tenant: $server) }}" wire:navigate>
<div <div
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg" class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
style="background-color: {{ $server->condition->getColor(true) }};"> style="background-color: {{ $server->condition->getColor(true) }};">
@ -44,4 +45,11 @@
</div> </div>
</div> </div>
</div> </div>
</a>
<x-filament-tables::actions
:actions="\App\Filament\App\Resources\ServerResource\Pages\ListServers::getPowerActions()"
:alignment="\Filament\Support\Enums\Alignment::Center"
:record="$server"
/>
</div> </div>