Update Overview, Again. Add some customization (#1200)

* wip

* wip

* wip

* overview 2.1

* Combine 2 branches into one

* updates

* Fix 500

* use my friend JSON

* Use switch
This commit is contained in:
Charles 2025-04-04 12:08:43 -04:00 committed by GitHub
parent 3639d7ccec
commit befe6be80b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 284 additions and 58 deletions

View File

@ -62,7 +62,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
self::Removing => 'warning', self::Removing => 'warning',
self::Missing => 'danger', self::Missing => 'danger',
self::Stopping => 'warning', self::Stopping => 'warning',
self::Offline => 'gray', self::Offline => 'danger',
}; };
} }

View File

@ -2,13 +2,16 @@
namespace App\Filament\App\Resources\ServerResource\Pages; namespace App\Filament\App\Resources\ServerResource\Pages;
use App\Enums\ServerResourceType;
use App\Filament\App\Resources\ServerResource; use App\Filament\App\Resources\ServerResource;
use App\Filament\Components\Tables\Columns\ServerEntryColumn; use App\Filament\Components\Tables\Columns\ServerEntryColumn;
use App\Filament\Server\Pages\Console; use App\Filament\Server\Pages\Console;
use App\Models\Server; use App\Models\Server;
use Filament\Resources\Components\Tab; use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\ColumnGroup;
use Filament\Tables\Columns\Layout\Stack; use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -17,21 +20,89 @@ class ListServers extends ListRecords
{ {
protected static string $resource = ServerResource::class; protected static string $resource = ServerResource::class;
public const DANGER_THRESHOLD = 0.9;
public const WARNING_THRESHOLD = 0.7;
public function table(Table $table): Table public function table(Table $table): Table
{ {
$baseQuery = auth()->user()->accessibleServers(); $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 return $table
->paginated(false) ->paginated(false)
->query(fn () => $baseQuery) ->query(fn () => $baseQuery)
->poll('15s') ->poll('15s')
->columns([ ->columns(
Stack::make([ (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid'
ServerEntryColumn::make('server_entry') ? [
->searchable(['name']), 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)) ->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!')
@ -73,4 +144,50 @@ class ListServers extends ListRecords
->badge($all->count()), ->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;
}
} }

View File

@ -29,6 +29,7 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Get; use Filament\Forms\Get;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Auth\EditProfile as BaseEditProfile; use Filament\Pages\Auth\EditProfile as BaseEditProfile;
@ -242,6 +243,7 @@ class EditProfile extends BaseEditProfile
->password(), ->password(),
]; ];
}), }),
Tab::make(trans('profile.tabs.api_keys')) Tab::make(trans('profile.tabs.api_keys'))
->icon('tabler-key') ->icon('tabler-key')
->schema([ ->schema([
@ -308,9 +310,11 @@ class EditProfile extends BaseEditProfile
]), ]),
]), ]),
]), ]),
Tab::make(trans('profile.tabs.ssh_keys')) Tab::make(trans('profile.tabs.ssh_keys'))
->icon('tabler-lock-code') ->icon('tabler-lock-code')
->hidden(), ->hidden(),
Tab::make(trans('profile.tabs.activity')) Tab::make(trans('profile.tabs.activity'))
->icon('tabler-history') ->icon('tabler-history')
->schema([ ->schema([
@ -325,6 +329,47 @@ class EditProfile extends BaseEditProfile
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())), 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') ->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;
}
} }

View File

@ -481,7 +481,7 @@ class Server extends Model implements Validatable
} }
if ($resourceAmount === 0 & $limit) { if ($resourceAmount === 0 & $limit) {
return 'Unlimited'; return "\u{221E}";
} }
if ($type === ServerResourceType::Percentage) { if ($type === ServerResourceType::Percentage) {

View File

@ -68,6 +68,7 @@ use Spatie\Permission\Traits\HasRoles;
* @property int|null $tokens_count * @property int|null $tokens_count
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Role[] $roles * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Role[] $roles
* @property int|null $roles_count * @property int|null $roles_count
* @property string|null $customization
* *
* @method static \Database\Factories\UserFactory factory(...$parameters) * @method static \Database\Factories\UserFactory factory(...$parameters)
* @method static Builder|User newModelQuery() * @method static Builder|User newModelQuery()
@ -125,6 +126,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'totp_authenticated_at', 'totp_authenticated_at',
'gravatar', 'gravatar',
'oauth', 'oauth',
'customization',
]; ];
/** /**
@ -142,6 +144,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'use_totp' => false, 'use_totp' => false,
'totp_secret' => null, 'totp_secret' => null,
'oauth' => '[]', 'oauth' => '[]',
'customization' => null,
]; ];
/** @var array<array-key, string[]> */ /** @var array<array-key, string[]> */
@ -156,6 +159,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'use_totp' => ['boolean'], 'use_totp' => ['boolean'],
'totp_secret' => ['nullable', 'string'], 'totp_secret' => ['nullable', 'string'],
'oauth' => ['array', 'nullable'], '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 protected function casts(): array
@ -166,6 +173,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'totp_authenticated_at' => 'datetime', 'totp_authenticated_at' => 'datetime',
'totp_secret' => 'encrypted', 'totp_secret' => 'encrypted',
'oauth' => 'array', 'oauth' => 'array',
'customization' => 'array',
]; ];
} }
@ -402,4 +410,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return false; return false;
} }
/** @return array<mixed> */
public function getCustomization(): array
{
return json_decode($this->customization, true) ?? [];
}
} }

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->json('customization')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('customization');
});
}
};

View File

@ -9,6 +9,7 @@ return [
'api_keys' => 'API Keys', 'api_keys' => 'API Keys',
'ssh_keys' => 'SSH Keys', 'ssh_keys' => 'SSH Keys',
'2fa' => '2FA', '2fa' => '2FA',
'customization' => 'Customization',
], ],
'username' => 'Username', 'username' => 'Username',
'exit_admin' => 'Exit Admin', 'exit_admin' => 'Exit Admin',
@ -38,4 +39,12 @@ return [
'description' => 'Description', 'description' => 'Description',
'allowed_ips' => 'Allowed IPs', 'allowed_ips' => 'Allowed IPs',
'allowed_ips_help' => 'Press enter to add a new IP address or leave blank to allow any IP address', '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',
]; ];

View File

@ -57,14 +57,14 @@
}; };
let options = { let options = {
fontSize: 14, fontSize: {{ auth()->user()->getCustomization()['console_font_size'] ?? 14 }},
fontFamily: 'Comic Mono, monospace', fontFamily: 'Comic Mono, monospace',
lineHeight: 1.2, lineHeight: 1.2,
disableStdin: true, disableStdin: true,
cursorStyle: 'underline', cursorStyle: 'underline',
cursorInactiveStyle: 'underline', cursorInactiveStyle: 'underline',
allowTransparency: true, allowTransparency: true,
rows: 30, rows: {{ auth()->user()->getCustomization()['console_rows'] ?? 30 }},
theme: theme theme: theme
}; };

View File

@ -19,57 +19,45 @@
style="background-color: {{ $server->condition->getColor(true) }};"> style="background-color: {{ $server->condition->getColor(true) }};">
</div> </div>
<div class="bg-gray-800 dark:text-white overflow-hidden p-2"> <div class="flex-1 bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
<div class="flex items-center justify-between"> <div class="flex items-center mb-5 gap-2">
<div class="flex items-center"> <x-filament::icon-button
<x-filament::icon-button :icon="$server->condition->getIcon()"
:icon="$server->condition->getIcon()" :color="$server->condition->getColor()"
:color="$server->condition->getColor()" :tooltip="$server->condition->getLabel()"
:tooltip="$server->condition->getLabel()" size="xl"
size="xl" />
/> <h2 class="text-xl font-bold">
<h2 class="text-2xl font-semibold p-2"> {{ $server->name }}
{{ $server->name }} <span class="dark:text-gray-400">({{ $server->formatResource('uptime', type: ServerResourceType::Time) }})</span>
<span class="dark:text-gray-400"> </h2>
({{ $server->formatResource('uptime', type: ServerResourceType::Time) }}) </div>
</span>
</h2>
</div>
<div class="flex w-1/2 justify-between text-center"> <div class="flex justify-between text-center">
<div class="w-1/4"> <div>
<p class="text-md dark:text-gray-400">CPU</p> <p class="text-sm dark:text-gray-400">CPU</p>
<hr class="p-0.5"> <p class="text-md font-semibold">{{ $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage) }}</p>
<p class="text-md font-semibold"> <hr class="p-0.5">
{{ $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage) }} <p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: ServerResourceType::Percentage, limit: true) }}</p>
</p> </div>
</div> <div>
<div class="w-1/4"> <p class="text-sm dark:text-gray-400">Memory</p>
<p class="text-md dark:text-gray-400">Memory</p> <p class="text-md font-semibold">{{ $server->formatResource('memory_bytes') }}</p>
<hr class="p-0.5"> <hr class="p-0.5">
<p class="text-md font-semibold"> <p class="text-xs dark:text-gray-400">{{ $server->formatResource('memory', limit: true) }}</p>
{{ $server->formatResource('memory_bytes') }} </div>
</p> <div>
</div> <p class="text-sm dark:text-gray-400">Disk</p>
<div class="w-1/4 hidden sm:block"> <p class="text-md font-semibold">{{ $server->formatResource('disk_bytes') }}</p>
<p class="text-md dark:text-gray-400">Disk</p> <hr class="p-0.5">
<hr class="p-0.5"> <p class="text-xs dark:text-gray-400">{{ $server->formatResource('disk', limit: true) }}</p>
<p class="text-md font-semibold"> </div>
{{ $server->formatResource('disk_bytes') }} <div class="hidden sm:block">
</p> <p class="text-sm dark:text-gray-400">Network</p>
</div> <hr class="p-0.5">
<div class="w-1/4 hidden sm:block"> <p class="text-md font-semibold">{{ $server->allocation->address }} </p>
<p class="text-md dark:text-gray-400">Network</p>
<hr class="p-0.5">
<p class="text-md font-semibold">
{{ $server->allocation->address }}
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>