diff --git a/app/Enums/ContainerStatus.php b/app/Enums/ContainerStatus.php index 79cabf490..b83a61021 100644 --- a/app/Enums/ContainerStatus.php +++ b/app/Enums/ContainerStatus.php @@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel 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 @@ -97,18 +97,16 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel return true; } - return !in_array($this, [ContainerStatus::Offline]); + return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]); } 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 { - // [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created] - - return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]); + return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]); } } diff --git a/app/Enums/ServerState.php b/app/Enums/ServerState.php index 0746bb724..9ada541ad 100644 --- a/app/Enums/ServerState.php +++ b/app/Enums/ServerState.php @@ -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) { self::Normal => 'primary', self::Installing => 'primary', diff --git a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php index 66651ab56..87f083943 100644 --- a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php @@ -9,12 +9,13 @@ use App\Filament\Server\Pages\Console; use App\Models\Permission; use App\Models\Server; use App\Repositories\Daemon\DaemonPowerRepository; -use AymanAlhattami\FilamentContextMenu\Columns\ContextMenuTextColumn; use Filament\Notifications\Notification; use Filament\Resources\Components\Tab; use Filament\Resources\Pages\ListRecords; +use Filament\Support\Enums\Alignment; 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\TextColumn; use Filament\Tables\Filters\SelectFilter; @@ -38,121 +39,73 @@ class ListServers extends ListRecords $this->daemonPowerRepository = new DaemonPowerRepository(); } - public function table(Table $table): Table + /** @return Stack[] */ + protected function gridColumns(): array { - $baseQuery = auth()->user()->accessibleServers(); + return [ + Stack::make([ + ServerEntryColumn::make('server_entry') + ->searchable(['name']), + ]), + ]; + } - $menuOptions = function (Server $server) { - $status = $server->retrieveStatus(); - - return [ - Action::make('start') - ->color('primary') - ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server)) - ->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 = [ - ContextMenuTextColumn::make('condition') - ->label('') - ->default('unknown') - ->wrap() + /** @return Column[] */ + protected function tableColumns(): array + { + return [ + TextColumn::make('condition') + ->label('Status') ->badge() - ->alignCenter() ->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time)) ->icon(fn (Server $server) => $server->condition->getIcon()) - ->color(fn (Server $server) => $server->condition->getColor()) - ->contextMenuActions($menuOptions) - ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()), - ]; - - $viewTwo = [ - ContextMenuTextColumn::make('name') - ->label('') - ->size('md') - ->searchable() - ->contextMenuActions($menuOptions) - ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()), - ContextMenuTextColumn::make('allocation.address') + ->color(fn (Server $server) => $server->condition->getColor()), + TextColumn::make('name') + ->label('Server') + ->description(fn (Server $server) => $server->description) + ->grow() + ->searchable(), + TextColumn::make('allocation.address') ->label('') ->badge() - ->copyable(request()->isSecure()) - ->contextMenuActions($menuOptions) - ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()), - ]; - - $viewThree = [ + ->visibleFrom('md') + ->copyable(request()->isSecure()), TextColumn::make('cpuUsage') - ->label('') + ->label('Resources') ->icon('tabler-cpu') ->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)) ->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')), TextColumn::make('memoryUsage') ->label('') - ->icon('tabler-memory') + ->icon('tabler-device-desktop-analytics') ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true)) ->state(fn (Server $server) => $server->formatResource('memory_bytes')) ->color(fn (Server $server) => $this->getResourceColor($server, 'memory')), TextColumn::make('diskUsage') ->label('') - ->icon('tabler-device-floppy') + ->icon('tabler-device-sd-card') ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true)) ->state(fn (Server $server) => $server->formatResource('disk_bytes')) ->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 ->paginated(false) ->query(fn () => $baseQuery) ->poll('15s') - ->columns( - (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid' - ? [ - Stack::make([ - ServerEntryColumn::make('server_entry') - ->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, - ]) + ->columns($usingGrid ? $this->gridColumns() : $this->tableColumns()) + ->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null) + ->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions()) : []) + ->actionsAlignment(Alignment::Center->value) + ->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null) ->emptyStateIcon('tabler-brand-docker') ->emptyStateDescription('') ->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; $limit = null; switch ($resource) { case 'cpu': - $current = $server->resources()['cpu_absolute'] ?? 0; + $current = $server->retrieveResources()['cpu_absolute'] ?? 0; $limit = $server->cpu; if ($server->cpu === 0) { return null; } break; - case 'memory': - $current = $server->resources()['memory_bytes'] ?? 0; + $current = $server->retrieveResources()['memory_bytes'] ?? 0; $limit = $server->memory * 2 ** 20; if ($server->memory === 0) { return null; } break; - case 'disk': - $current = $server->resources()['disk_bytes'] ?? 0; + $current = $server->retrieveResources()['disk_bytes'] ?? 0; $limit = $server->disk * 2 ** 20; if ($server->disk === 0) { return null; } break; - default: return null; } @@ -238,7 +188,6 @@ class ListServers extends ListRecords } return null; - } #[On('powerAction')] @@ -253,6 +202,8 @@ class ListServers extends ListRecords ->success() ->send(); + cache()->forget("servers.$server->uuid.status"); + $this->redirect(self::getUrl(['activeTab' => $this->activeTab])); } catch (ConnectionException) { Notification::make() @@ -261,4 +212,36 @@ class ListServers extends ListRecords ->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']), + ]; + } } diff --git a/app/Models/Node.php b/app/Models/Node.php index c6ffb655d..e3ff28d88 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -333,26 +333,6 @@ class Node extends Model implements Validatable }); } - /** - * @return array - */ - 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{ * memory_total: int, memory_used: int, * swap_total: int, swap_used: int, diff --git a/app/Models/Server.php b/app/Models/Server.php index 0115b1bbf..5ba473418 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -435,25 +435,24 @@ class Server extends Model implements Validatable 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) { - $this->node->serverStatuses(); - - $status = cache()->get("servers.$this->uuid.container.status"); - } - - return ContainerStatus::tryFrom($status) ?? ContainerStatus::Missing; + return ContainerStatus::tryFrom(Arr::get($details, 'state')) ?? ContainerStatus::Missing; + }); } /** * @return array */ - 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 - 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; if (!$limit) { - $resourceAmount = $this->resources()[$resourceKey] ?? 0; + $resourceAmount = $this->retrieveResources()[$resourceKey] ?? 0; } if ($type === ServerResourceType::Time) { @@ -494,7 +493,7 @@ class Server extends Model implements Validatable public function condition(): Attribute { return Attribute::make( - get: fn () => $this->isSuspended() ? ServerState::Suspended : $this->status ?? $this->retrieveStatus(), + get: fn () => $this->status ?? $this->retrieveStatus(), ); } } diff --git a/composer.json b/composer.json index 93430c549..fe3ef777e 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,6 @@ "ext-zip": "*", "abdelhamiderrahmouni/filament-monaco-editor": "^0.2.5", "aws/aws-sdk-php": "^3.344", - "aymanalhattami/filament-context-menu": "^1.0", "calebporzio/sushi": "^2.5", "chillerlan/php-qrcode": "^5.0.2", "dedoc/scramble": "^0.12.10", @@ -105,4 +104,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index b4673ed7d..507c44250 100644 --- a/composer.lock +++ b/composer.lock @@ -1113,83 +1113,7 @@ "issues": "https://github.com/aws/aws-sdk-php/issues", "source": "https://github.com/aws/aws-sdk-php/tree/3.344.3" }, - "time": "2025-06-09T18:04:12+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" + "time": "2025-06-02T18:04:47+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -15981,7 +15905,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -15992,9 +15916,9 @@ "ext-pdo": "*", "ext-zip": "*" }, - "platform-dev": {}, + "platform-dev": [], "platform-overrides": { "php": "8.2" }, "plugin-api-version": "2.6.0" -} +} \ No newline at end of file diff --git a/resources/views/livewire/server-entry.blade.php b/resources/views/livewire/server-entry.blade.php index 27fb2def0..6f4b25775 100644 --- a/resources/views/livewire/server-entry.blade.php +++ b/resources/views/livewire/server-entry.blade.php @@ -1,47 +1,55 @@ -