diff --git a/app/Enums/ContainerStatus.php b/app/Enums/ContainerStatus.php index 2bcf5c130..79cabf490 100644 --- a/app/Enums/ContainerStatus.php +++ b/app/Enums/ContainerStatus.php @@ -62,7 +62,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel self::Removing => 'warning', self::Missing => 'danger', self::Stopping => 'warning', - self::Offline => 'gray', + self::Offline => 'danger', }; } diff --git a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php index 3d6a48fd8..a961d5036 100644 --- a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php @@ -2,13 +2,16 @@ namespace App\Filament\App\Resources\ServerResource\Pages; +use App\Enums\ServerResourceType; use App\Filament\App\Resources\ServerResource; use App\Filament\Components\Tables\Columns\ServerEntryColumn; use App\Filament\Server\Pages\Console; use App\Models\Server; use Filament\Resources\Components\Tab; use Filament\Resources\Pages\ListRecords; +use Filament\Tables\Columns\ColumnGroup; 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; @@ -17,21 +20,89 @@ class ListServers extends ListRecords { protected static string $resource = ServerResource::class; + public const DANGER_THRESHOLD = 0.9; + + public const WARNING_THRESHOLD = 0.7; + public function table(Table $table): Table { $baseQuery = auth()->user()->accessibleServers(); + $viewOne = [ + TextColumn::make('condition') + ->label('') + ->default('unknown') + ->wrap() + ->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()), + ]; + + $viewTwo = [ + TextColumn::make('name') + ->label('') + ->size('md') + ->searchable(), + TextColumn::make('') + ->label('') + ->badge() + ->copyable(request()->isSecure()) + ->copyMessage(fn (Server $server, string $state) => 'Copied ' . $server->allocation->address) + ->state(fn (Server $server) => $server->allocation->address), + ]; + + $viewThree = [ + TextColumn::make('cpuUsage') + ->label('') + ->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') + ->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') + ->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')), + ]; + return $table ->paginated(false) ->query(fn () => $baseQuery) ->poll('15s') - ->columns([ - Stack::make([ - ServerEntryColumn::make('server_entry') - ->searchable(['name']), - ]), - ]) + ->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, + ]) ->emptyStateIcon('tabler-brand-docker') ->emptyStateDescription('') ->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!') @@ -73,4 +144,50 @@ class ListServers extends ListRecords ->badge($all->count()), ]; } + + public function getResourceColor(Server $server, string $resource): ?string + { + $current = null; + $limit = null; + + switch ($resource) { + case 'cpu': + $current = $server->resources()['cpu_absolute'] ?? 0; + $limit = $server->cpu; + if ($server->cpu === 0) { + return null; + } + break; + + case 'memory': + $current = $server->resources()['memory_bytes'] ?? 0; + $limit = $server->memory * 2 ** 20; + if ($server->memory === 0) { + return null; + } + break; + + case 'disk': + $current = $server->resources()['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; + + } } diff --git a/app/Filament/Pages/Auth/EditProfile.php b/app/Filament/Pages/Auth/EditProfile.php index 1b0da3890..c0f21801e 100644 --- a/app/Filament/Pages/Auth/EditProfile.php +++ b/app/Filament/Pages/Auth/EditProfile.php @@ -29,6 +29,7 @@ use Filament\Forms\Components\Tabs\Tab; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Get; use Filament\Notifications\Notification; use Filament\Pages\Auth\EditProfile as BaseEditProfile; @@ -242,6 +243,7 @@ class EditProfile extends BaseEditProfile ->password(), ]; }), + Tab::make(trans('profile.tabs.api_keys')) ->icon('tabler-key') ->schema([ @@ -308,9 +310,11 @@ class EditProfile extends BaseEditProfile ]), ]), ]), + Tab::make(trans('profile.tabs.ssh_keys')) ->icon('tabler-lock-code') ->hidden(), + Tab::make(trans('profile.tabs.activity')) ->icon('tabler-history') ->schema([ @@ -325,6 +329,47 @@ class EditProfile extends BaseEditProfile Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())), ]), ]), + + Tab::make(trans('profile.tabs.customization')) + ->icon('tabler-adjustments') + ->schema([ + Section::make(trans('profile.dashboard')) + ->collapsible() + ->icon('tabler-dashboard') + ->schema([ + ToggleButtons::make('dashboard_layout') + ->label(trans('profile.dashboard_layout')) + ->inline() + ->required() + ->options([ + 'grid' => trans('profile.grid'), + 'table' => trans('profile.table'), + ]), + ]), + Section::make(trans('profile.console')) + ->collapsible() + ->icon('tabler-brand-tabler') + ->schema([ + TextInput::make('console_rows') + ->label(trans('profile.rows')) + ->minValue(1) + ->numeric() + ->required() + ->columnSpan(1) + ->default(30), + // Select::make('console_font') + // ->label(trans('profile.font')) + // ->hidden() //TODO + // ->columnSpan(1), + TextInput::make('console_font_size') + ->label(trans('profile.font_size')) + ->columnSpan(1) + ->minValue(1) + ->numeric() + ->required() + ->default(14), + ]), + ]), ]), ]) ->operation('edit') @@ -381,4 +426,29 @@ class EditProfile extends BaseEditProfile ]; } + + protected function mutateFormDataBeforeSave(array $data): array + { + $moarbetterdata = [ + 'console_font_size' => $data['console_font_size'], + 'console_rows' => $data['console_rows'], + 'dashboard_layout' => $data['dashboard_layout'], + ]; + + unset($data['dashboard_layout'], $data['console_font_size'], $data['console_rows']); + $data['customization'] = json_encode($moarbetterdata); + + return $data; + } + + protected function mutateFormDataBeforeFill(array $data): array + { + $moarbetterdata = json_decode($data['customization'], true); + + $data['console_font_size'] = $moarbetterdata['console_font_size'] ?? 14; + $data['console_rows'] = $moarbetterdata['console_rows'] ?? 30; + $data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid'; + + return $data; + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index f93131556..9d61c4310 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -481,7 +481,7 @@ class Server extends Model implements Validatable } if ($resourceAmount === 0 & $limit) { - return 'Unlimited'; + return "\u{221E}"; } if ($type === ServerResourceType::Percentage) { diff --git a/app/Models/User.php b/app/Models/User.php index 3fb70b70d..3254526ba 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -68,6 +68,7 @@ use Spatie\Permission\Traits\HasRoles; * @property int|null $tokens_count * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Role[] $roles * @property int|null $roles_count + * @property string|null $customization * * @method static \Database\Factories\UserFactory factory(...$parameters) * @method static Builder|User newModelQuery() @@ -125,6 +126,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'totp_authenticated_at', 'gravatar', 'oauth', + 'customization', ]; /** @@ -142,6 +144,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'use_totp' => false, 'totp_secret' => null, 'oauth' => '[]', + 'customization' => null, ]; /** @var array */ @@ -156,6 +159,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'use_totp' => ['boolean'], 'totp_secret' => ['nullable', 'string'], 'oauth' => ['array', 'nullable'], + 'customization' => ['array', 'nullable'], + 'customization.console_rows' => ['integer', 'min:1'], + 'customization.console_font' => ['string'], + 'customization.console_font_size' => ['integer', 'min:1'], ]; protected function casts(): array @@ -166,6 +173,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'totp_authenticated_at' => 'datetime', 'totp_secret' => 'encrypted', 'oauth' => 'array', + 'customization' => 'array', ]; } @@ -402,4 +410,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return false; } + + /** @return array */ + public function getCustomization(): array + { + return json_decode($this->customization, true) ?? []; + } } diff --git a/database/migrations/2025_04_01_214953_add_customization_column.php b/database/migrations/2025_04_01_214953_add_customization_column.php new file mode 100644 index 000000000..51ff112fb --- /dev/null +++ b/database/migrations/2025_04_01_214953_add_customization_column.php @@ -0,0 +1,28 @@ +json('customization')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('customization'); + }); + } +}; diff --git a/lang/en/profile.php b/lang/en/profile.php index daea83628..e6354118c 100644 --- a/lang/en/profile.php +++ b/lang/en/profile.php @@ -9,6 +9,7 @@ return [ 'api_keys' => 'API Keys', 'ssh_keys' => 'SSH Keys', '2fa' => '2FA', + 'customization' => 'Customization', ], 'username' => 'Username', 'exit_admin' => 'Exit Admin', @@ -38,4 +39,12 @@ return [ 'description' => 'Description', 'allowed_ips' => 'Allowed IPs', 'allowed_ips_help' => 'Press enter to add a new IP address or leave blank to allow any IP address', + 'dashboard' => 'Dashboard', + 'dashboard_layout' => 'Dashboard Layout', + 'console' => 'Console', + 'grid' => 'Grid', + 'table' => 'Table', + 'rows' => 'Rows', + 'font_size' => 'Font Size', + 'font' => 'Font', ]; diff --git a/resources/views/filament/components/server-console.blade.php b/resources/views/filament/components/server-console.blade.php index 9cbdb8c2a..a2e19f487 100644 --- a/resources/views/filament/components/server-console.blade.php +++ b/resources/views/filament/components/server-console.blade.php @@ -57,14 +57,14 @@ }; let options = { - fontSize: 14, + fontSize: {{ auth()->user()->getCustomization()['console_font_size'] ?? 14 }}, fontFamily: 'Comic Mono, monospace', lineHeight: 1.2, disableStdin: true, cursorStyle: 'underline', cursorInactiveStyle: 'underline', allowTransparency: true, - rows: 30, + rows: {{ auth()->user()->getCustomization()['console_rows'] ?? 30 }}, theme: theme }; diff --git a/resources/views/tables/columns/server-entry-column.blade.php b/resources/views/tables/columns/server-entry-column.blade.php index 11eef6ae2..152db35b6 100644 --- a/resources/views/tables/columns/server-entry-column.blade.php +++ b/resources/views/tables/columns/server-entry-column.blade.php @@ -19,57 +19,45 @@ style="background-color: {{ $server->condition->getColor(true) }};"> -
-
-
- -

- {{ $server->name }} - - ({{ $server->formatResource('uptime', type: ServerResourceType::Time) }}) - -

-
+
+
+ +

+ {{ $server->name }} + ({{ $server->formatResource('uptime', type: ServerResourceType::Time) }}) +

+
-
-
-

CPU

-
-

- {{ $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage) }} -

-
-
-

Memory

-
-

- {{ $server->formatResource('memory_bytes') }} -

-
- - +
+
+

CPU

+

{{ $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage) }}

+
+

{{ $server->formatResource('cpu', type: ServerResourceType::Percentage, limit: true) }}

+
+
+

Memory

+

{{ $server->formatResource('memory_bytes') }}

+
+

{{ $server->formatResource('memory', limit: true) }}

+
+
+

Disk

+

{{ $server->formatResource('disk_bytes') }}

+
+

{{ $server->formatResource('disk', limit: true) }}

+
+
- - -