Compare commits

...

76 Commits

Author SHA1 Message Date
Charles
dfd6dbfe26 Update Stock Egg Images (#1973) 2025-12-09 17:53:07 -05:00
Charles
b4f331e4b2 composer update (#1972) 2025-12-09 17:09:06 -05:00
Charles
7a95712ed0 composer update (#1966) 2025-12-08 10:46:33 -05:00
MartinOscar
b6aeb954c4 Disable Captcha & Oauth Settings actions when read only (#1968) 2025-12-08 11:33:29 +01:00
MartinOscar
7c0d53c796 Use Policies rather then overriding can*() functions (#1837)
Co-authored-by: Boy132 <mail@boy132.de>
2025-12-07 14:53:13 -05:00
MartinOscar
71bd267166 Fix docker entrypoint ASSET_URL not APP_ASSET (#1965) 2025-12-06 20:54:40 +01:00
MartinOscar
25d8adbcc6 Add ignoreRecord to CopyFrom relationships (#1964) 2025-12-06 20:17:05 +01:00
Michael (Parker) Parker
27b896c6d2 Update docker image (#1917)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-12-05 22:50:49 -05:00
MartinOscar
bda2f9a699 Fix Save Notification icon & Cleanup (#1959) 2025-12-03 02:23:09 +01:00
Boy132
04375439d7 Add pagination to server list (#1955) 2025-12-02 08:26:45 +01:00
Boy132
0fe8917668 Only allow server transfers to accessible nodes (#1951) 2025-12-02 08:26:19 +01:00
Boy132
c312ef493f Replace file_get_contents with Http (#1953) 2025-12-02 08:25:53 +01:00
PalmarHealer
6c02f9a663 feat: Add toggle for automatic allocation creation in panel settings (#1884) 2025-12-01 08:59:07 +01:00
Charles
2dd6e3d4fc Add progress bars to client area (#1924) 2025-11-28 18:04:40 -05:00
Quinten
575e5bdb0d Fix typo in suspend method documentation (#1944) 2025-11-28 18:39:49 +01:00
Boy132
efa8eef57c Add custom render hooks to our footer (#1942) 2025-11-27 23:55:59 +01:00
MartinOscar
d16e7dd876 Better Role icons (#1936)
Fix `Role` class path for `::getNavigationIcon()`
Allow to register custom model icons
Co-authored-by: Boy132 <mail@boy132.de>
2025-11-27 23:51:57 +01:00
Charles
897b95ec13 Change Admin Actions to IconButtons (#1900) 2025-11-27 16:44:05 -05:00
MartinOscar
97f5a0f20b Fix Policies modelname are case sensitive (#1937) 2025-11-27 17:51:16 +01:00
MartinOscar
d0af45a0c7 Delete ssh keys shouldn't be a POST & Cleanup routes (#1934) 2025-11-27 16:26:47 +01:00
MartinOscar
78ab098d02 Fix Egg select_startup default & update state (#1933) 2025-11-27 16:26:40 +01:00
Charles
cdccca8fa2 composer update (#1928) 2025-11-24 15:34:33 -05:00
Boy132
bb33bcca4f Refactor schedule tasks (#1911) 2025-11-24 14:42:47 +01:00
Boy132
611b8649e0 Improve "first task" checks (#1926) 2025-11-24 00:48:32 +01:00
MartinOscar
b1b723485f Fix EditFiles breadcrumbs incorrect url (#1925) 2025-11-24 00:42:04 +01:00
hallo123wert
25c8ff3f1f Fix: No live preview for fonts (#1921) 2025-11-24 00:06:08 +01:00
Boy132
07763d912b Add back 2fa requirement middleware (#1897)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-11-24 00:01:29 +01:00
Charles
65bb99e2b0 Add server icons (#1906)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-11-21 16:48:20 -05:00
MartinOscar
a195b56f93 Fix permission checks on Client side (#1913) 2025-11-19 22:28:13 +01:00
Boy132
d78c977d75 Make sure to load FilamentServiceProvider before panel providers (#1907) 2025-11-17 11:41:11 +01:00
PalmarHealer
5e25ea4a43 fix: use port range on free allocation lookup (#1882)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-11-17 10:56:48 +01:00
Luke
886836c60a Remove 'required' rule from egg-garrys-mod.yaml (#1902) 2025-11-16 11:59:01 -05:00
Charles
f575e3edfa composer update (#1901) 2025-11-15 07:17:29 -05:00
Boy132
1a66b3fab4 Encode file contents to utf-8 (#1896) 2025-11-13 19:05:23 +01:00
Boy132
0f1efcfd15 Remove old update command (#1898) 2025-11-13 19:05:04 +01:00
PalmarHealer
3f89c6ddd8 fix: bypass tenant scoping in allocation queries (#1883) 2025-11-13 04:48:25 +00:00
mristau
20cb7850ef don't try to bulk update if egg doesn't even have a url (#1887) 2025-11-13 04:47:38 +00:00
hallo123wert
108dad09fb Fix: Duplicate bulk deletion notifications (#1881) 2025-11-13 04:46:55 +00:00
Boy132
445c9364bc Make sure case for role permissions is correct (#1892) 2025-11-11 18:18:29 +01:00
MartinOscar
acec117b1e Use public disk for console fonts upload (#1893) 2025-11-11 18:13:52 +01:00
Boy132
89199dfbe5 Fix jar mime type (#1891) 2025-11-11 11:23:56 +01:00
Boy132
216a3484f1 Fix node_ids rule for database host (#1885) 2025-11-10 12:25:58 +01:00
Boy132
5c3b0919aa Fix allocations by admins aren't locked by default (#1879) 2025-11-09 18:29:46 +01:00
Charles
f4ee33fa4f Hide new allocation action if server has 0 allocations. (#1878) 2025-11-09 12:11:14 -05:00
Charles
d8368c4cec Do no use stock notifications on actions (#1877) 2025-11-09 12:08:25 -05:00
Charles
aa35d7d001 Fix creating mounts (#1876) 2025-11-09 11:14:44 -05:00
JoanFo
3c25b43b46 Repair webhooks once again (#1815)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-11-09 09:35:00 -05:00
Charles
0891db5342 Reimplement Drag & Drop for file uploading 🎉 (#1858) 2025-11-09 09:24:12 -05:00
exefer
172436e012 Fix typo in failed upload message (#1874) 2025-11-09 12:58:56 +00:00
Charles
2b5403a4da Replace current panel log viewer with new and improved log viewer (#1834) 2025-11-08 19:31:51 -05:00
Charles
a30c45fbbe Add session key to use last used node, instead of latest created node (#1869)
Co-authored-by: Lance Pioch <git@lance.sh>
2025-11-08 17:09:41 -05:00
Copilot
b06df23823 Add bulk IP update action for node allocations (#1845)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com>
Co-authored-by: Charles <charles@pelican.dev>
2025-11-08 16:53:12 -05:00
exefer
1ff965611e Fix typo in DNS help text (#1868)
Authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-11-08 22:40:23 +01:00
Boy132
cec141889a Allow admins to "lock" allocations (#1811) 2025-11-08 21:54:41 +01:00
Charles
6ed84b5584 Add wings diagnostics retrieving to Edit Node page (#1865)
Co-authored-by: Boy132 <mail@boy132.de>
2025-11-08 15:47:40 -05:00
Lance Pioch
49f24e37b6 Laravel 12.37.0 Shift (#1864)
Co-authored-by: Shift <shift@laravelshift.com>
2025-11-06 08:43:02 -05:00
Boy132
e0c4e47a6c Fix directAccessibleServers returning duplicates (#1862) 2025-11-05 16:19:03 +01:00
Boy132
4bda7cba75 Allow to "embed" server list (#1860) 2025-11-05 16:18:44 +01:00
Boy132
852f7beb39 Allow to register "special file" alert banners (#1861) 2025-11-04 12:48:18 +01:00
mristau
d61583cd7b add server description to grid view too (#1851) 2025-11-04 06:03:50 -05:00
Charles
21f9f259d0 Add Egg Images (#1849) 2025-11-03 12:32:11 -05:00
M41den
b2aff5445b Fix admin serverlist search (#1854) 2025-11-03 06:50:08 -05:00
Boy132
1f26750a2a Add api endpoint for updating username (#1826) 2025-11-03 08:31:07 +01:00
Charles
6d83c6d908 composer update (#1856) 2025-11-02 18:53:24 -05:00
Copilot
574a391e73 Add border-radius to activity log avatars (#1848)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com>
2025-11-02 15:13:36 -05:00
PalmarHealer
605fcbe61a feat: Add mixed navigation type with admin-configurable defaults (#1850) 2025-10-31 14:12:54 -04:00
Letter N
0214b127e4 Add setup wizard to all oauth providers (#1801) 2025-10-31 14:09:20 -04:00
MartinOscar
e6aa76ef2c Refactor: add FilamentServiceProvider & globally make Select native(false) (#1836) 2025-10-29 23:23:18 +01:00
Boy132
d38075e3cb Add boolean cast to read_only toggle buttons (#1844) 2025-10-28 16:06:33 +01:00
M41den
0fec6adc3e Fix 500 "No route found" when creating db host (#1841) 2025-10-28 08:48:46 -04:00
M41den
5e3c22ea5e Fix weird postgres behavior when selecting mounts (#1842)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-10-28 08:48:35 -04:00
MartinOscar
d1a808a746 Hide User reset password Action on create Operation (#1840) 2025-10-28 01:38:37 +01:00
MartinOscar
3bcdeea800 Leverage user() helper (#1832) 2025-10-26 16:24:34 +01:00
Charles
e6bd6e416f Add archive extension selection (#1828) 2025-10-24 12:39:30 -04:00
Boy132
8e006ac32d Fix user permissions service (#1819) 2025-10-22 16:00:51 +02:00
Boy132
430f28a847 Add "cancel" button to profile (#1821) 2025-10-22 16:00:31 +02:00
224 changed files with 6466 additions and 2203 deletions

1
.gitignore vendored
View File

@@ -21,6 +21,7 @@ yarn-error.log
/.idea
/.nova
/.vscode
/.ddev
public/assets/manifest.json
/database/*.sqlite*

View File

@@ -7,6 +7,7 @@ use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use JsonException;
use Symfony\Component\Yaml\Yaml;
@@ -44,9 +45,7 @@ class CheckEggUpdatesCommand extends Command
? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML))
: json_decode($exporterService->handle($egg->id, EggFormat::JSON), true);
$remote = file_get_contents($egg->update_url);
assert($remote !== false);
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url)->throw()->body();
$remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
unset($local['exported_at'], $remote['exported_at']);

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands\Egg;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class UpdateEggIndexCommand extends Command
{
@@ -12,8 +13,7 @@ class UpdateEggIndexCommand extends Command
public function handle(): int
{
try {
$data = file_get_contents('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json');
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
$data = Http::timeout(5)->connectTimeout(1)->get('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json')->throw()->json();
} catch (Exception $exception) {
$this->error($exception->getMessage());

View File

@@ -1,196 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Console\Kernel;
use Closure;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Process\Process;
class UpgradeCommand extends Command
{
protected const DEFAULT_URL = 'https://github.com/pelican-dev/panel/releases/%s/panel.tar.gz';
protected $signature = 'p:upgrade
{--user= : The user that PHP runs under. All files will be owned by this user.}
{--group= : The group that PHP runs under. All files will be owned by this group.}
{--url= : The specific archive to download.}
{--release= : A specific version to download from GitHub. Leave blank to use latest.}
{--skip-download : If set no archive will be downloaded.}';
protected $description = 'Downloads a new archive from GitHub and then executes the normal upgrade commands.';
/**
* Executes an upgrade command which will run through all of our standard
* Panel commands and enable users to basically just download
* the archive and execute this and be done.
*
* This places the application in maintenance mode as well while the commands
* are being executed.
*
* @throws Exception
*/
public function handle(): void
{
$skipDownload = $this->option('skip-download');
if (!$skipDownload) {
$this->output->warning(trans('commands.upgrade.integrity'));
$this->output->comment(trans('commands.upgrade.source_url'));
$this->line($this->getUrl());
}
$user = 'www-data';
$group = 'www-data';
if ($this->input->isInteractive()) {
if (!$skipDownload) {
$skipDownload = !$this->confirm(trans('commands.upgrade.skipDownload'), true);
}
if (is_null($this->option('user'))) {
$userDetails = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner('public')) : [];
$user = $userDetails['name'] ?? 'www-data';
$message = trans('commands.upgrade.webserver_user', ['user' => $user]);
if (!$this->confirm($message, true)) {
$user = $this->anticipate(
trans('commands.upgrade.name_webserver'),
[
'www-data',
'nginx',
'apache',
]
);
}
}
if (is_null($this->option('group'))) {
$groupDetails = function_exists('posix_getgrgid') ? posix_getgrgid(filegroup('public')) : [];
$group = $groupDetails['name'] ?? 'www-data';
$message = trans('commands.upgrade.group_webserver', ['group' => $user]);
if (!$this->confirm($message, true)) {
$group = $this->anticipate(
trans('commands.upgrade.group_webserver_question'),
[
'www-data',
'nginx',
'apache',
]
);
}
}
if (!$this->confirm(trans('commands.upgrade.are_your_sure'))) {
$this->warn(trans('commands.upgrade.terminated'));
return;
}
}
ini_set('output_buffering', '0');
$bar = $this->output->createProgressBar($skipDownload ? 9 : 10);
$bar->start();
if (!$skipDownload) {
$this->withProgress($bar, function () {
$this->line("\$upgrader> curl -L \"{$this->getUrl()}\" | tar -xzv");
$process = Process::fromShellCommandline("curl -L \"{$this->getUrl()}\" | tar -xzv");
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
}
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan down');
$this->call('down');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> chmod -R 755 storage bootstrap/cache');
$process = new Process(['chmod', '-R', '755', 'storage', 'bootstrap/cache']);
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
$this->withProgress($bar, function () {
$command = ['composer', 'install', '--no-ansi'];
if (config('app.env') === 'production' && !config('app.debug')) {
$command[] = '--optimize-autoloader';
$command[] = '--no-dev';
}
$this->line('$upgrader> ' . implode(' ', $command));
$process = new Process($command);
$process->setTimeout(10 * 60);
$process->run(function ($type, $buffer) {
$this->line($buffer);
});
});
/** @var Application $app */
$app = require __DIR__ . '/../../../bootstrap/app.php';
/** @var Kernel $kernel */
$kernel = $app->make(Kernel::class);
$kernel->bootstrap();
$this->setLaravel($app);
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan view:clear');
$this->call('view:clear');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan config:clear');
$this->call('config:clear');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan migrate --force --seed');
$this->call('migrate', ['--force' => true, '--seed' => true]);
});
$this->withProgress($bar, function () use ($user, $group) {
$this->line("\$upgrader> chown -R {$user}:{$group} *");
$process = Process::fromShellCommandline("chown -R {$user}:{$group} *", $this->getLaravel()->basePath());
$process->setTimeout(10 * 60);
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan queue:restart');
$this->call('queue:restart');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan up');
$this->call('up');
});
$this->newLine(2);
$this->info(trans('commands.upgrade.success'));
}
protected function withProgress(ProgressBar $bar, Closure $callback): void
{
$bar->clear();
$callback();
$bar->advance();
$bar->display();
}
protected function getUrl(): string
{
if ($this->option('url')) {
return $this->option('url');
}
return sprintf(self::DEFAULT_URL, $this->option('release') ? 'download/v' . $this->option('release') : 'latest/download');
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum CustomRenderHooks: string
{
case FooterStart = 'pelican::footer.start';
case FooterEnd = 'pelican::footer.end';
}

View File

@@ -18,7 +18,7 @@ enum CustomizationKey: string
self::ConsoleFont => 'monospace',
self::ConsoleFontSize => 14,
self::ConsoleGraphPeriod => 30,
self::TopNavigation => false,
self::TopNavigation => config('panel.filament.default-navigation', 'sidebar'),
self::DashboardLayout => 'grid',
};
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Exceptions\Http;
use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class TwoFactorAuthRequiredException extends HttpException implements HttpExceptionInterface
{
/**
* TwoFactorAuthRequiredException constructor.
*/
public function __construct(?\Throwable $previous = null)
{
parent::__construct(Response::HTTP_BAD_REQUEST, 'Two-factor authentication is required on this account in order to access this endpoint.', $previous);
}
}

View File

@@ -56,8 +56,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
->default(fn () => $server->image)
->notIn(fn () => $server->image)
->required()
->preload()
->native(false),
->preload(),
])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) {
try {

View File

@@ -4,6 +4,10 @@ namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Authentik\Provider;
final class AuthentikSchema extends OAuthSchema
@@ -20,11 +24,27 @@ final class AuthentikSchema extends OAuthSchema
public function getServiceConfig(): array
{
return [
return array_merge(parent::getServiceConfig(), [
'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'),
'client_id' => env('OAUTH_AUTHENTIK_CLIENT_ID'),
'client_secret' => env('OAUTH_AUTHENTIK_CLIENT_SECRET'),
];
]);
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Create Authentik Application')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>On your Authentik dashboard select <b>Applications</b>, then select <b>Create with Provider</b>.</p><p>On the creation step select <b>OAuth2/OpenID Provider</b> and on the configure step set <b>Redirect URIs/Origins</b> to the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/authentik')),
]),
], parent::getSetupSteps());
}
public function getSettingsForm(): array

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class BitbucketSchema extends OAuthSchema
{
public function getId(): string
{
return 'bitbucket';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Bitbucket Consumer')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud" target="_blank">Bitbucket OAuth Documentation</x-filament::link> and follow the steps in <b>Create a consumer</b>.</p><p>For the <b>Callback URL</b> use the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/bitbucket')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-bitbucket-f';
}
public function getHexColor(): string
{
return '#205081';
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class FacebookSchema extends OAuthSchema
{
public function getId(): string
{
return 'facebook';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Facebook Application')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://developers.facebook.com/apps" target="_blank">Facebook Developer Dashboard</x-filament::link> and select or create a new app you will use for authentication. Make sure to have "Authenticate and request data from users with Facebook Login" as one of the Use Cases.</p><p>Once selected go to <b>Use Cases</b> and customize "Authenticate and request data from users with Facebook Login", from there go to <b>Settings</b> and add <b>Valid OAuth Redirect URIs</b> using the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Valid OAuth Redirect URIs')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/facebook')),
TextEntry::make('get_app_info')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>To obtain the OAuth values go to <b>App Settings > Basic</b>.</p>'))),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-facebook-f';
}
public function getHexColor(): string
{
return '#1877f2';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class GoogleSchema extends OAuthSchema
{
public function getId(): string
{
return 'google';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new OAuth client')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://console.developers.google.com/" target="_blank">Google API Console</x-filament::link> and create or select the project you want to use.</p><p>Navigate or search <b>Credentials</b>, click on the <b>Create Credentials</b> button and select <b>OAuth client ID</b>. On the Application type select <b>Web Application</b>.</p><p>On <b>Authorized JavaScript origins</b> and <b>Authorized redirect URIs</b> add and use the values below.</p>'))),
TextInput::make('_noenv_origin')
->label('Authorized JavaScript origins')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('')),
TextInput::make('_noenv_callback')
->label('Authorized redirect URIs')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/google')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>When you filled all fields click on <b>Create</b>.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-google-f';
}
public function getHexColor(): string
{
return '#4285f4';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class LinkedinSchema extends OAuthSchema
{
public function getId(): string
{
return 'linkedin';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Obtain Linkedin App OAuth Config')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p><x-filament::link href="https://www.linkedin.com/developers/apps/new" target="_blank">Create</x-filament::link> or <x-filament::link href="https://www.linkedin.com/developers/apps" target="_blank">select</x-filament::link> the one you will be using for authentication.</p><p>Select the <b>Auth</b> tab and set <b>Authorized redirect URLs for your app</b> to the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorized redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/linkedin')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-linkedin-f';
}
public function getHexColor(): string
{
return '#0a66c2';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class SlackSchema extends OAuthSchema
{
public function getId(): string
{
return 'slack';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Slack OAuth')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p><x-filament::link href="https://api.slack.com/apps?new_app=1" target="_blank">Create</x-filament::link> a slack app or <x-filament::link href="https://api.slack.com/apps" target="_blank">select</x-filament::link> the one you will be using for authentication.</p><p>Navigate to the <b>OAuth & Permissions</b> section and configure the <b>Redirect URL</b> using the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/slack')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-slack';
}
public function getHexColor(): string
{
return '#6ecadc';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class XSchema extends OAuthSchema
{
public function getId(): string
{
return 'x';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new X App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://developer.x.com/en/portal/dashboard" target="_blank">X Developer Dashboard</x-filament::link> and create or select the project app you want to use.</p><p>Go to the app\'s settings and set up <b>User authentication</b> if not yet. Make sure to select <b>Web App</b> as the type of app.</p><p>For the <b>Callback URI / Redirect URL</b> and <b>Website URL</b> set it using the value below.</p>'))),
TextInput::make('_noenv_origin')
->label('Website URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('')),
TextInput::make('_noenv_callback')
->label('Callback URI / Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/x')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>If you have already set this up go to your app\'s <b>Keys and tokens</b> and obtain the Client ID and Secret there.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-x';
}
public function getHexColor(): string
{
return '#1da1f2';
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Schedule;
use App\Models\Task;
use App\Services\Backups\InitiateBackupService;
final class CreateBackupSchema extends TaskSchema
{
public function __construct(private InitiateBackupService $backupService) {}
public function getId(): string
{
return 'backup';
}
public function runTask(Task $task): void
{
$this->backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($task->server, null, true);
}
public function canCreate(Schedule $schedule): bool
{
return $schedule->server->backup_limit > 0;
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.backup.files_to_ignore');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Task;
use App\Services\Files\DeleteFilesService;
final class DeleteFilesSchema extends TaskSchema
{
public function __construct(private DeleteFilesService $deleteFilesService) {}
public function getId(): string
{
return 'delete_files';
}
public function runTask(Task $task): void
{
$this->deleteFilesService->handle($task->server, explode(PHP_EOL, $task->payload));
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.delete_files.files_to_delete');
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Task;
use App\Repositories\Daemon\DaemonServerRepository;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Component;
use Illuminate\Support\Str;
final class PowerActionSchema extends TaskSchema
{
public function __construct(private DaemonServerRepository $serverRepository) {}
public function getId(): string
{
return 'power';
}
public function runTask(Task $task): void
{
$this->serverRepository->setServer($task->server)->power($task->payload);
}
public function getDefaultPayload(): string
{
return 'restart';
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.power.action');
}
public function formatPayload(string $payload): string
{
return Str::ucfirst($payload);
}
/** @return Component[] */
public function getPayloadForm(): array
{
return [
Select::make('payload')
->label($this->getPayloadLabel())
->required()
->options([
'start' => trans('server/schedule.tasks.actions.power.start'),
'restart' => trans('server/schedule.tasks.actions.power.restart'),
'stop' => trans('server/schedule.tasks.actions.power.stop'),
'kill' => trans('server/schedule.tasks.actions.power.kill'),
])
->selectablePlaceholder(false)
->default($this->getDefaultPayload()),
];
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Task;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Component;
final class SendCommandSchema extends TaskSchema
{
public function getId(): string
{
return 'command';
}
public function runTask(Task $task): void
{
$task->server->send($task->payload);
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.command.command');
}
/** @return Component[] */
public function getPayloadForm(): array
{
return [
TextInput::make('payload')
->required()
->label($this->getPayloadLabel())
->default($this->getDefaultPayload()),
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Extensions\Tasks\TaskSchemaInterface;
use App\Models\Schedule;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Components\Component;
abstract class TaskSchema implements TaskSchemaInterface
{
public function getName(): string
{
return trans('server/schedule.tasks.actions.' . $this->getId() . '.title');
}
public function canCreate(Schedule $schedule): bool
{
return true;
}
public function getDefaultPayload(): ?string
{
return null;
}
public function getPayloadLabel(): ?string
{
return null;
}
/** @return null|string|string[] */
public function formatPayload(string $payload): null|string|array
{
if (empty($payload)) {
return null;
}
return explode(PHP_EOL, $payload);
}
/** @return Component[] */
public function getPayloadForm(): array
{
return [
Textarea::make('payload')
->label($this->getPayloadLabel() ?? trans('server/schedule.tasks.payload'))
->default($this->getDefaultPayload())
->autosize(),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Extensions\Tasks;
use App\Models\Schedule;
use App\Models\Task;
use Filament\Schemas\Components\Component;
interface TaskSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function runTask(Task $task): void;
public function canCreate(Schedule $schedule): bool;
public function getDefaultPayload(): ?string;
public function getPayloadLabel(): ?string;
/** @return null|string|string[] */
public function formatPayload(string $payload): null|string|array;
/** @return Component[] */
public function getPayloadForm(): array;
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Extensions\Tasks;
class TaskService
{
/** @var array<string, TaskSchemaInterface> */
private array $schemas = [];
/**
* @return TaskSchemaInterface[]
*/
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?TaskSchemaInterface
{
return array_get($this->schemas, $id);
}
public function register(TaskSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/** @return array<string, string> */
public function getMappings(): array
{
return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all();
}
}

View File

@@ -6,6 +6,7 @@ use Carbon\Carbon;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Artisan;
use Spatie\Health\Commands\RunHealthChecksCommand;
use Spatie\Health\Enums\Status;
@@ -47,7 +48,8 @@ class Health extends Page
return [
Action::make('refresh')
->label(trans('admin/health.refresh'))
->button()
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-refresh')
->action('refresh'),
];
}

View File

@@ -0,0 +1,129 @@
<?php
namespace App\Filament\Admin\Pages;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Actions\ViewLogAction;
use Boquizo\FilamentLogViewer\Pages\ListLogs as BaseListLogs;
use Boquizo\FilamentLogViewer\Tables\Columns\LevelColumn;
use Boquizo\FilamentLogViewer\Tables\Columns\NameColumn;
use Boquizo\FilamentLogViewer\Utils\Level;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Http;
class ListLogs extends BaseListLogs
{
protected string $view = 'filament.components.list-logs';
public function getHeading(): string|null|\Illuminate\Contracts\Support\Htmlable
{
return trans('admin/log.navigation.panel_logs');
}
public static function table(Table $table): Table
{
return parent::table($table)
->emptyStateHeading(trans('admin/log.empty_table'))
->emptyStateIcon('tabler-check')
->columns([
NameColumn::make('date'),
LevelColumn::make(Level::ALL)
->tooltip(trans('admin/log.total_logs')),
LevelColumn::make(Level::Error)
->tooltip(trans('admin/log.error')),
LevelColumn::make(Level::Warning)
->tooltip(trans('admin/log.warning')),
LevelColumn::make(Level::Notice)
->tooltip(trans('admin/log.notice')),
LevelColumn::make(Level::Info)
->tooltip(trans('admin/log.info')),
LevelColumn::make(Level::Debug)
->tooltip(trans('admin/log.debug')),
])
->recordActions([
ViewLogAction::make()
->icon('tabler-file-description')->iconSize(IconSize::Large)->iconButton(),
DownloadAction::make()
->icon('tabler-file-download')->iconSize(IconSize::Large)->iconButton(),
Action::make('uploadLogs')
->hiddenLabel()
->icon('tabler-world-upload')->iconSize(IconSize::Large)->iconButton()
->requiresConfirmation()
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn ($record) => trans('admin/log.actions.upload_logs_description', ['file' => $record['date'], 'url' => 'https://logs.pelican.dev']))
->action(function ($record) {
$logPath = storage_path('logs/' . $record['date']);
if (!file_exists($logPath)) {
Notification::make()
->title(trans('admin/log.actions.log_not_found'))
->body(trans('admin/log.actions.log_not_found_description', ['filename' => $record['date']]))
->danger()
->send();
return;
}
$lines = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$totalLines = count($lines);
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body(trans('admin/log.actions.failed_to_upload_description', ['status' => $response->status()]))
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/log.actions.log_upload'))
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body($e->getMessage())
->danger()
->send();
return;
}
}),
DeleteAction::make()
->icon('tabler-trash')->iconSize(IconSize::Medium)->iconButton(),
]);
}
}

View File

@@ -36,6 +36,7 @@ use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\Width;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\Arr;
@@ -181,7 +182,6 @@ class Settings extends Page implements HasSchemas
->schema([
Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider'))
->native(false)
->options($this->avatarService->getMappings())
->selectablePlaceholder(false)
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
@@ -204,6 +204,15 @@ class Settings extends Page implements HasSchemas
])
->stateCast(new BooleanStateCast(false, true))
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
ToggleButtons::make('FILAMENT_DEFAULT_NAVIGATION')
->label(trans('admin/setting.general.default_navigation'))
->inline()
->options([
'sidebar' => trans('admin/setting.general.sidebar'),
'topbar' => trans('admin/setting.general.topbar'),
'mixed' => trans('admin/setting.general.mixed'),
])
->default(env('FILAMENT_DEFAULT_NAVIGATION', config('panel.filament.default-navigation'))),
ToggleButtons::make('APP_2FA_REQUIRED')
->label(trans('admin/setting.general.2fa_requirement'))
->inline()
@@ -217,7 +226,6 @@ class Settings extends Page implements HasSchemas
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
Select::make('FILAMENT_WIDTH')
->label(trans('admin/setting.general.display_width'))
->native(false)
->options(Width::class)
->selectablePlaceholder(false)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
@@ -289,11 +297,13 @@ class Settings extends Page implements HasSchemas
Actions::make([
Action::make("disable_captcha_$id")
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.captcha.disable'))
->color('danger')
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", false)),
Action::make("enable_captcha_$id")
->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED"))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.captcha.enable'))
->color('success')
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", true)),
@@ -560,11 +570,13 @@ class Settings extends Page implements HasSchemas
Actions::make([
Action::make("disable_oauth_$id")
->visible(fn (Get $get) => $get($key))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.oauth.disable'))
->color('danger')
->action(fn (Set $set) => $set($key, false)),
Action::make("enable_oauth_$id")
->visible(fn (Get $get) => !$get($key))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.oauth.enable'))
->color('success')
->steps($schema->getSetupSteps())
@@ -615,6 +627,18 @@ class Settings extends Page implements HasSchemas
->columnSpanFull()
->stateCast(new BooleanStateCast(false))
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
Toggle::make('PANEL_CLIENT_ALLOCATIONS_CREATE_NEW')
->label(trans('admin/setting.misc.auto_allocation.create_new'))
->helperText(trans('admin/setting.misc.auto_allocation.create_new_help'))
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
->stateCast(new BooleanStateCast(false))
->default(env('PANEL_CLIENT_ALLOCATIONS_CREATE_NEW', config('panel.client_features.allocations.create_new'))),
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
->label(trans('admin/setting.misc.auto_allocation.start'))
->required()
@@ -749,6 +773,7 @@ class Settings extends Page implements HasSchemas
->hint(trans('admin/setting.misc.server.console_font_hint'))
->label(trans('admin/setting.misc.server.console_font_upload'))
->directory('fonts')
->disk('public')
->columnSpan(1)
->maxFiles(1)
->preserveFilenames(),
@@ -821,6 +846,8 @@ class Settings extends Page implements HasSchemas
{
return [
Action::make('save')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy')
->action('save')
->authorize(fn () => user()?->can('update settings'))
->keyBindings(['mod+s']),

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Filament\Admin\Pages;
use App\Traits\ResolvesRecordDate;
use Boquizo\FilamentLogViewer\Actions\BackAction;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Pages\ViewLog as BaseViewLog;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Http;
class ViewLogs extends BaseViewLog
{
use ResolvesRecordDate;
public function getHeaderActions(): array
{
return [
BackAction::make()
->icon('tabler-arrow-left')->iconSize(IconSize::ExtraLarge)->iconButton(),
DeleteAction::make(withTooltip: true)
->icon('tabler-trash')->iconSize(IconSize::ExtraLarge)->iconButton(),
DownloadAction::make(withTooltip: true)
->icon('tabler-file-download')->iconSize(IconSize::ExtraLarge)->iconButton(),
Action::make('uploadLogs')
->hiddenLabel()
->icon('tabler-world-upload')->iconSize(IconSize::ExtraLarge)->iconButton()
->requiresConfirmation()
->tooltip(trans('admin/log.actions.upload_tooltip', ['url' => 'logs.pelican.dev']))
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn () => trans('admin/log.actions.upload_logs_description', ['file' => $this->resolveRecordDate(), 'url' => 'https://logs.pelican.dev']))
->action(function () {
$logPath = storage_path('logs/' . $this->resolveRecordDate());
if (!file_exists($logPath)) {
Notification::make()
->title(trans('admin/log.actions.log_not_found'))
->body(trans('admin/log.actions.log_not_found_description', ['filename' => $this->resolveRecordDate()]))
->danger()
->send();
return;
}
$lines = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$totalLines = count($lines);
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body(trans('admin/log.actions.failed_to_upload_description', ['status' => $response->status()]))
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/log.actions.log_upload'))
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body($e->getMessage())
->danger()
->send();
return;
}
}),
];
}
}

View File

@@ -12,7 +12,6 @@ use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
@@ -21,6 +20,7 @@ use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
@@ -96,14 +96,12 @@ class ApiKeyResource extends Resource
->url(fn (ApiKey $apiKey) => user()?->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
])
->recordActions([
DeleteAction::make(),
DeleteAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
])
->emptyStateIcon('tabler-key')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/apikey.empty'))
->emptyStateActions([
CreateAction::make(),
]);
->emptyStateHeading(trans('admin/apikey.empty'));
}
/**

View File

@@ -9,6 +9,7 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@@ -25,7 +26,9 @@ class CreateApiKey extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}

View File

@@ -3,13 +3,13 @@
namespace App\Filament\Admin\Resources\ApiKeys\Pages;
use App\Filament\Admin\Resources\ApiKeys\ApiKeyResource;
use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListApiKeys extends ListRecords
{
@@ -23,7 +23,8 @@ class ListApiKeys extends ListRecords
{
return [
CreateAction::make()
->hidden(fn () => ApiKey::where('key_type', ApiKey::TYPE_APPLICATION)->count() <= 0),
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -13,7 +13,6 @@ use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
@@ -92,7 +91,7 @@ class DatabaseHostResource extends Resource
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::canEdit($record)),
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
])
->groupedBulkActions([
@@ -100,10 +99,7 @@ class DatabaseHostResource extends Resource
])
->emptyStateIcon('tabler-database')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/databasehost.no_database_hosts'))
->emptyStateActions([
CreateAction::make(),
]);
->emptyStateHeading(trans('admin/databasehost.no_database_hosts'));
}
/**

View File

@@ -12,6 +12,7 @@ use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Model;
use PDOException;
@@ -36,8 +37,11 @@ class EditDatabaseHost extends EditRecord
return [
DeleteAction::make()
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? trans('admin/databasehost.delete_help') : trans('filament-actions::delete.single.modal.actions.delete.label'))
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
$this->getSaveFormAction()->formId('form'),
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0)
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}

View File

@@ -3,13 +3,13 @@
namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Models\DatabaseHost;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListDatabaseHosts extends ListRecords
{
@@ -23,7 +23,8 @@ class ListDatabaseHosts extends ListRecords
{
return [
CreateAction::make()
->hidden(fn () => DatabaseHost::count() <= 0),
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -9,6 +9,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewDatabaseHost extends ViewRecord
{
@@ -21,7 +22,8 @@ class ViewDatabaseHost extends ViewRecord
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),
EditAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}
}

View File

@@ -10,6 +10,7 @@ use Filament\Actions\ViewAction;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -69,7 +70,8 @@ class DatabasesRelationManager extends RelationManager
->recordActions([
ViewAction::make()
->color('primary'),
DeleteAction::make(),
DeleteAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
]);
}
}

View File

@@ -27,6 +27,7 @@ use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Unique;
@@ -44,7 +45,9 @@ class CreateEgg extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
@@ -124,7 +127,7 @@ class CreateEgg extends CreateRecord
->keyLabel(trans('admin/egg.docker_name'))
->keyPlaceholder('Java 21')
->valueLabel(trans('admin/egg.docker_uri'))
->valuePlaceholder('ghcr.io/parkervcp/yolks:java_21')
->valuePlaceholder('ghcr.io/pelican-eggs/yolks:java_21')
->helperText(trans('admin/egg.docker_help')),
]),
@@ -257,7 +260,6 @@ class CreateEgg extends CreateRecord
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->native(false)
->selectablePlaceholder(false)
->default('bash')
->options([

View File

@@ -16,6 +16,7 @@ use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
@@ -24,13 +25,19 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Flex;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Validation\Rules\Unique;
class EditEgg extends EditRecord
@@ -50,36 +57,215 @@ class EditEgg extends EditRecord
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon('tabler-egg')
->schema([
Grid::make(2)
->columnSpan(1)
->schema([
Image::make('', '')
->hidden(fn ($record) => !$record->image)
->url(fn ($record) => $record->image)
->alt('')
->alignJustify()
->imageSize(150)
->columnSpanFull(),
Flex::make([
Action::make('uploadImage')
->iconButton()
->iconSize(IconSize::Large)
->icon('tabler-photo-up')
->modal()
->modalHeading('')
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('base64Image'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
return;
}
try {
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
$allowedExtensions = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
];
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, $allowedExtensions)) {
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', $allowedExtensions)]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new \Exception(trans('admin/egg.import.no_local_ip'));
}
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$imageContent = @file_get_contents($state, false, $context, 0, 1048576); // 1024KB
if (!$imageContent) {
throw new \Exception(trans('admin/egg.import.image_error'));
}
if (strlen($imageContent) >= 1048576) {
throw new \Exception(trans('admin/egg.import.image_too_large'));
}
$mimeType = $allowedExtensions[$extension];
$base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent);
$set('base64Image', $base64);
$set('image_url_error', null);
} catch (\Exception $e) {
$set('image_url_error', $e->getMessage());
$set('base64Image', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn ($get) => $get('image_url_error') !== null)
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(1024)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->saveUploadedFileUsing(function ($file, Set $set) {
$base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath()));
$set('base64Image', $base64);
return $base64;
}),
]),
]),
])
->action(function (array $data, $record): void {
$base64 = $data['base64Image'] ?? null;
if (empty($base64) && !empty($data['image'])) {
$base64 = $data['image'];
}
if (!empty($base64)) {
$record->update([
'image' => $base64,
]);
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
$record->refresh();
} else {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('deleteImage')
->visible(fn ($record) => $record->image)
->label('')
->icon('tabler-trash')
->iconButton()
->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
$record->update([
'image' => null,
]);
Notification::make()
->title(trans('admin/egg.import.image_deleted'))
->success()
->send();
$record->refresh();
}),
]),
]),
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
->helperText(trans('admin/egg.description_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->columnSpan(1)
->disabled(),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->disabled(),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.description_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
@@ -93,24 +279,20 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
@@ -248,7 +430,6 @@ class EditEgg extends EditRecord
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->native(false)
->selectablePlaceholder(false)
->options([
'bash' => 'bash',
@@ -270,11 +451,14 @@ class EditEgg extends EditRecord
return [
DeleteAction::make()
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')),
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use'))
->iconButton()->iconSize(IconSize::ExtraLarge),
ExportEggAction::make(),
ImportEggAction::make()
->multiple(false),
$this->getSaveFormAction()->formId('form'),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}

View File

@@ -19,6 +19,8 @@ use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ReplicateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
@@ -42,6 +44,13 @@ class ListEggs extends ListRecords
TextColumn::make('id')
->label('Id')
->hidden(),
ImageColumn::make('image')
->label('')
->alignCenter()
->circular()
->getStateUsing(fn ($record) => $record->image
? $record->image
: 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')))),
TextColumn::make('name')
->label(trans('admin/egg.name'))
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
@@ -55,16 +64,18 @@ class ListEggs extends ListRecords
->recordActions([
EditAction::make()
->iconButton()
->tooltip(trans('filament-actions::edit.single.label')),
->tooltip(trans('filament-actions::edit.single.label'))
->iconSize(IconSize::Large),
ExportEggAction::make()
->iconButton()
->tooltip(trans('filament-actions::export.modal.actions.export.label')),
->tooltip(trans('filament-actions::export.modal.actions.export.label'))
->iconSize(IconSize::Large),
UpdateEggAction::make()
->iconButton()
->tooltip(trans_choice('admin/egg.update', 1)),
->tooltip(trans_choice('admin/egg.update', 1))
->iconSize(IconSize::Large),
ReplicateAction::make()
->iconButton()
->tooltip(trans('filament-actions::replicate.single.label'))
->iconSize(IconSize::Large)
->modal(false)
->excludeAttributes(['author', 'uuid', 'update_url', 'servers_count', 'created_at', 'updated_at'])
->beforeReplicaSaved(function (Egg $replica) {
@@ -90,11 +101,6 @@ class ListEggs extends ListRecords
->emptyStateIcon('tabler-eggs')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/egg.no_eggs'))
->emptyStateActions([
CreateAction::make(),
ImportEggAction::make()
->multiple(),
])
->filters([
TagsFilter::make()
->model(Egg::class),
@@ -109,7 +115,9 @@ class ListEggs extends ListRecords
return [
ImportEggAction::make()
->multiple(),
CreateAction::make(),
CreateAction::make()
->icon('tabler-file-plus')
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}
}

View File

@@ -12,7 +12,6 @@ use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
@@ -24,6 +23,7 @@ use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -95,7 +95,7 @@ class MountResource extends Resource
])
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::canEdit($record)),
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
])
->groupedBulkActions([
@@ -103,10 +103,7 @@ class MountResource extends Resource
])
->emptyStateIcon('tabler-layers-linked')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/mount.no_mounts'))
->emptyStateActions([
CreateAction::make(),
]);
->emptyStateHeading(trans('admin/mount.no_mounts'));
}
/**
@@ -125,6 +122,7 @@ class MountResource extends Resource
ToggleButtons::make('read_only')
->label(trans('admin/mount.read_only'))
->helperText(trans('admin/mount.read_only_help'))
->stateCast(new BooleanStateCast(false, true))
->options([
false => trans('admin/mount.toggles.writable'),
true => trans('admin/mount.toggles.read_only'),
@@ -138,8 +136,7 @@ class MountResource extends Resource
true => 'success',
])
->inline()
->default(false)
->required(),
->default(false),
TextInput::make('source')
->label(trans('admin/mount.source'))
->required()
@@ -162,7 +159,8 @@ class MountResource extends Resource
Section::make()->schema([
Select::make('eggs')->multiple()
->label(trans('admin/mount.eggs'))
->relationship('eggs', 'name')
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
->preload(),
Select::make('nodes')->multiple()
->label(trans('admin/mount.nodes'))

View File

@@ -8,6 +8,7 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@@ -24,7 +25,9 @@ class CreateMount extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}

View File

@@ -9,6 +9,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
class EditMount extends EditRecord
{
@@ -21,8 +22,11 @@ class EditMount extends EditRecord
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make(),
$this->getSaveFormAction()->formId('form'),
DeleteAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}

View File

@@ -3,13 +3,13 @@
namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Models\Mount;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListMounts extends ListRecords
{
@@ -23,7 +23,8 @@ class ListMounts extends ListRecords
{
return [
CreateAction::make()
->hidden(fn () => Mount::count() <= 0),
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -9,6 +9,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewMount extends ViewRecord
{
@@ -21,7 +22,9 @@ class ViewMount extends ViewRecord
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),
EditAction::make()
->iconSize(IconSize::ExtraLarge)
->iconButton(),
];
}
}

View File

@@ -7,7 +7,7 @@ use App\Filament\Admin\Resources\Nodes\Pages\CreateNode;
use App\Filament\Admin\Resources\Nodes\Pages\EditNode;
use App\Filament\Admin\Resources\Nodes\Pages\ListNodes;
use App\Filament\Admin\Resources\Nodes\RelationManagers\AllocationsRelationManager;
use App\Filament\Admin\Resources\Nodes\RelationManagers\NodesRelationManager;
use App\Filament\Admin\Resources\Nodes\RelationManagers\ServersRelationManager;
use App\Models\Node;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
@@ -57,7 +57,7 @@ class NodeResource extends Resource
{
return [
AllocationsRelationManager::class,
NodesRelationManager::class,
ServersRelationManager::class,
];
}

View File

@@ -19,6 +19,7 @@ use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Wizard;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
@@ -400,14 +401,16 @@ class CreateNode extends CreateRecord
]),
]),
])->columnSpanFull()
->nextAction(fn (Action $action) => $action->label(trans('admin/node.next_step')))
->nextAction(fn (Action $action) => $action->label(trans('admin/node.next_step'))->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-right'))
->previousAction(fn (Action $action) => $action->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-left'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
<x-filament::icon-button
type="submit"
size="sm"
iconSize="xl"
icon="tabler-file-plus"
>
{{ trans('admin/node.create') }}
</x-filament::button>
</x-filament::icon-button>
BLADE))),
]);
}

View File

@@ -4,7 +4,7 @@ namespace App\Filament\Admin\Resources\Nodes\Pages;
use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Models\Node;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Repositories\Daemon\DaemonSystemRepository;
use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService;
@@ -14,6 +14,8 @@ use Exception;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
@@ -25,6 +27,7 @@ use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
@@ -33,7 +36,10 @@ use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
use Filament\Support\RawJs;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
use Phiki\Grammar\Grammar;
use Throwable;
@@ -45,13 +51,13 @@ class EditNode extends EditRecord
protected static string $resource = NodeResource::class;
private DaemonConfigurationRepository $daemonConfigurationRepository;
private DaemonSystemRepository $daemonSystemRepository;
private NodeUpdateService $nodeUpdateService;
public function boot(DaemonConfigurationRepository $daemonConfigurationRepository, NodeUpdateService $nodeUpdateService): void
public function boot(DaemonSystemRepository $daemonSystemRepository, NodeUpdateService $nodeUpdateService): void
{
$this->daemonConfigurationRepository = $daemonConfigurationRepository;
$this->daemonSystemRepository = $daemonSystemRepository;
$this->nodeUpdateService = $nodeUpdateService;
}
@@ -624,6 +630,154 @@ class EditNode extends EditRecord
])->fullWidth(),
]),
]),
Tab::make('diagnostics')
->label(trans('admin/node.tabs.diagnostics'))
->icon('tabler-heart-search')
->schema([
Section::make('diag')
->heading(trans('admin/node.tabs.diagnostics'))
->columnSpanFull()
->columns(4)
->disabled(fn (Get $get) => $get('pulled'))
->headerActions([
Action::make('pull')
->label(trans('admin/node.diagnostics.pull'))
->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge)
->hidden(fn (Get $get) => $get('pulled'))
->action(function (Get $get, Set $set, Node $node) {
$includeEndpoints = $get('include_endpoints') ?? true;
$includeLogs = $get('include_logs') ?? true;
$logLines = $get('log_lines') ?? 200;
try {
$response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs);
if ($response->status() === 404) {
Notification::make()
->title(trans('admin/node.diagnostics.404'))
->warning()
->send();
return;
}
$set('pulled', true);
$set('uploaded', false);
$set('log', $response->body());
Notification::make()
->title(trans('admin/node.diagnostics.logs_pulled'))
->success()
->send();
} catch (ConnectionException $e) {
Notification::make()
->title(trans('admin/node.error_connecting', ['node' => $node->name]))
->body($e->getMessage())
->danger()
->send();
}
}),
Action::make('upload')
->label(trans('admin/node.diagnostics.upload'))
->visible(fn (Get $get) => $get('pulled') ?? false)
->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge)
->action(function (Get $get, Set $set) {
try {
$response = Http::asMultipart()->post('https://logs.pelican.dev', [
[
'name' => 'c',
'contents' => $get('log'),
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
->title(trans('admin/node.diagnostics.upload_failed'))
->body(fn () => $response->status() . ' - ' . $response->body())
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/node.diagnostics.logs_uploaded'))
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
->label(trans('admin/node.diagnostics.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
$set('log', $url);
$set('pulled', false);
$set('uploaded', true);
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/node.diagnostics.upload_failed'))
->body($e->getMessage())
->danger()
->send();
}
}),
Action::make('clear')
->label(trans('admin/node.diagnostics.clear'))
->visible(fn (Get $get) => $get('pulled') ?? false)
->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger')
->action(function (Get $get, Set $set) {
$set('pulled', false);
$set('uploaded', false);
$set('log', null);
$this->refresh();
}
),
])
->schema([
ToggleButtons::make('include_endpoints')
->hintIcon('tabler-question-mark')->inline()
->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint'))
->formatStateUsing(fn () => 1)
->boolean(),
ToggleButtons::make('include_logs')
->live()
->hintIcon('tabler-question-mark')->inline()
->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint'))
->formatStateUsing(fn () => 1)
->boolean(),
Slider::make('log_lines')
->columnSpan(2)
->hiddenLabel()
->live()
->tooltips(RawJs::make(<<<'JS'
`${$value} lines`
JS))
->visible(fn (Get $get) => $get('include_logs'))
->range(minValue: 100, maxValue: 500)
->pips(PipsMode::Steps, density: 10)
->step(50)
->formatStateUsing(fn () => 200)
->fillTrack(),
Hidden::make('pulled'),
Hidden::make('uploaded'),
]),
Textarea::make('log')
->hiddenLabel()
->columnSpanFull()
->rows(35)
->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)),
]),
]),
]);
}
@@ -656,8 +810,11 @@ class EditNode extends EditRecord
return [
DeleteAction::make()
->disabled(fn (Node $node) => $node->servers()->count() > 0)
->label(fn (Node $node) => $node->servers()->count() > 0 ? trans('admin/node.node_has_servers') : trans('filament-actions::delete.single.label')),
$this->getSaveFormAction()->formId('form'),
->label(fn (Node $node) => $node->servers()->count() > 0 ? trans('admin/node.node_has_servers') : trans('filament-actions::delete.single.label'))
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}
@@ -681,7 +838,7 @@ class EditNode extends EditRecord
try {
if ($changed) {
$this->daemonConfigurationRepository->setNode($node)->update($node);
$this->daemonSystemRepository->setNode($node)->update($node);
}
parent::getSavedNotification()?->send();
} catch (ConnectionException) {

View File

@@ -13,6 +13,7 @@ use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -67,9 +68,6 @@ class ListNodes extends ListRecords
->emptyStateIcon('tabler-server-2')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/node.no_nodes'))
->emptyStateActions([
CreateAction::make(),
])
->filters([
TagsFilter::make()
->model(Node::class),
@@ -81,7 +79,8 @@ class ListNodes extends ListRecords
{
return [
CreateAction::make()
->hidden(fn () => Node::count() <= 0),
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Nodes\RelationManagers;
use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
use App\Filament\Components\Actions\UpdateNodeAllocations;
use App\Models\Allocation;
use App\Models\Node;
use App\Services\Allocations\AssignmentService;
@@ -15,6 +16,7 @@ use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
@@ -80,9 +82,13 @@ class AllocationsRelationManager extends RelationManager
->searchable()
->label(trans('admin/node.table.ip')),
])
->headerActions([
->toolbarActions([
DeleteBulkAction::make()
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
Action::make('create new allocation')
->label(trans('admin/node.create_allocation'))
->icon('tabler-world-plus')
->iconButton()->iconSize(IconSize::ExtraLarge)
->schema(fn () => [
Select::make('allocation_ip')
->options(fn () => collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
@@ -118,9 +124,8 @@ class AllocationsRelationManager extends RelationManager
->required(),
])
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
])
->groupedBulkActions([
DeleteBulkAction::make()
UpdateNodeAllocations::make()
->nodeRecord($this->getOwnerRecord())
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
]);
}

View File

@@ -2,13 +2,14 @@
namespace App\Filament\Admin\Resources\Nodes\RelationManagers;
use App\Enums\ServerResourceType;
use App\Models\Server;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class NodesRelationManager extends RelationManager
class ServersRelationManager extends RelationManager
{
protected static string $relationship = 'servers';
@@ -42,11 +43,18 @@ class NodesRelationManager extends RelationManager
->label(trans('admin/node.primary_allocation'))
->disabled(fn (Server $server) => $server->allocations->count() <= 1)
->options(fn (Server $server) => $server->allocations->take(1)->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
->selectablePlaceholder(fn (SelectColumn $select) => !$select->isDisabled())
->placeholder(trans('admin/node.none'))
->selectablePlaceholder(fn (Server $server) => $server->allocations->count() <= 1)
->placeholder(trans('admin/server.none'))
->sortable(),
TextColumn::make('memory')->label(trans('admin/node.memory')),
TextColumn::make('cpu')->label(trans('admin/node.cpu')),
TextColumn::make('cpu')
->label(trans('admin/node.cpu'))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::CPULimit)),
TextColumn::make('memory')
->label(trans('admin/node.memory'))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::MemoryLimit)),
TextColumn::make('disk')
->label(trans('admin/node.disk'))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::DiskLimit)),
TextColumn::make('databases_count')
->counts('databases')
->label(trans('admin/node.databases'))

View File

@@ -9,6 +9,7 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Spatie\Permission\Models\Permission;
@@ -31,7 +32,9 @@ class CreateRole extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-plus'),
];
}

View File

@@ -10,6 +10,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Spatie\Permission\Models\Permission;
@@ -57,8 +58,12 @@ class EditRole extends EditRecord
{
return [
DeleteAction::make()
->label(fn (Role $role) => $role->isRootAdmin() ? trans('admin/role.root_admin_delete') : ($role->users_count >= 1 ? trans('admin/role.in_use') : trans('filament-actions::delete.single.label')))
->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1)
->label(fn (Role $role) => $role->isRootAdmin() ? trans('admin/role.root_admin_delete') : ($role->users_count >= 1 ? trans('admin/role.in_use') : trans('filament-actions::delete.single.label'))), $this->getSaveFormAction()->formId('form'),
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}

View File

@@ -9,6 +9,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListRoles extends ListRecords
{
@@ -21,7 +22,9 @@ class ListRoles extends ListRecords
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make(),
CreateAction::make()
->icon('tabler-file-plus')
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}
}

View File

@@ -9,6 +9,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewRole extends ViewRecord
{
@@ -21,7 +22,8 @@ class ViewRole extends ViewRecord
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),
EditAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}
}

View File

@@ -15,7 +15,6 @@ use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
@@ -98,18 +97,12 @@ class RoleResource extends Resource
])
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::canEdit($record)),
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
])
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0)
->groupedBulkActions([
DeleteBulkAction::make(),
])
->emptyStateIcon('tabler-users-group')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/role.no_roles'))
->emptyStateActions([
CreateAction::make(),
]);
}
@@ -124,7 +117,7 @@ class RoleResource extends Resource
$options = [];
foreach ($permissions as $permission) {
$options[$permission . ' ' . strtolower($model)] = Str::headline($permission);
$options[$permission . ' ' . $model] = Str::headline($permission);
}
$permissionSections[] = self::makeSection($model, $options);
@@ -167,23 +160,11 @@ class RoleResource extends Resource
*/
private static function makeSection(string $model, array $options): Section
{
$model = ucwords($model);
$icon = null;
if (class_exists('\App\Filament\Admin\Resources\\' . $model . 'Resource')) {
$icon = ('\App\Filament\Admin\Resources\\' . $model . 'Resource')::getNavigationIcon();
} elseif (class_exists('\App\Filament\Admin\Pages\\' . $model)) {
$icon = ('\App\Filament\Admin\Pages\\' . $model)::getNavigationIcon();
} elseif (class_exists('\App\Filament\Server\Resources\\' . $model . 'Resource')) {
$icon = ('\App\Filament\Server\Resources\\' . $model . 'Resource')::getNavigationIcon();
}
return Section::make(Str::headline($model))
->columnSpan(1)
->collapsible()
->collapsed()
->icon($icon)
->icon(Role::getModelIcon($model))
->headerActions([
Action::make('count')
->label(fn (Get $get) => count($get(strtolower($model) . '_list')))

View File

@@ -116,6 +116,14 @@ class CreateServer extends CreateRecord
->prefixIcon('tabler-server-2')
->selectablePlaceholder(false)
->default(function () {
$lastUsedNode = session()->get('last_utilized_node');
if ($lastUsedNode && user()?->accessibleNodes()->where('id', $lastUsedNode)->exists()) {
$this->node = Node::find($lastUsedNode);
return $this->node?->id;
}
/** @var ?Node $latestNode */
$latestNode = user()?->accessibleNodes()->latest()->first();
$this->node = $latestNode;
@@ -405,6 +413,7 @@ class CreateServer extends CreateRecord
Select::make('select_startup')
->label(trans('admin/server.startup_cmd'))
->hidden(fn (Get $get) => $get('egg_id') === null)
->required()
->live()
->afterStateUpdated(fn (Set $set, $state) => $set('startup', $state))
->options(function ($state, Get $get, Set $set) {
@@ -418,7 +427,7 @@ class CreateServer extends CreateRecord
$set('select_startup', $currentStartup);
}
return array_flip($startups) + ['' => 'Custom Startup'];
return array_flip($startups) + ['custom' => 'Custom Startup'];
})
->selectablePlaceholder(false)
->columnSpanFull(),
@@ -436,7 +445,7 @@ class CreateServer extends CreateRecord
if (in_array($state, $startups)) {
$set('select_startup', $state);
} else {
$set('select_startup', '');
$set('select_startup', 'custom');
}
})
->placeholder(trans('admin/server.startup_placeholder'))
@@ -829,6 +838,8 @@ class CreateServer extends CreateRecord
$data['allocation_additional'] = collect($allocation_additional)->filter()->all();
}
session()->put('last_utilized_node', $data['node_id']);
try {
return $this->serverCreationService->handle($data);
} catch (Exception $exception) {

View File

@@ -12,7 +12,6 @@ use App\Filament\Components\StateCasts\ServerConditionStateCast;
use App\Filament\Server\Pages\Console;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\Node;
use App\Models\Server;
use App\Models\User;
use App\Repositories\Daemon\DaemonServerRepository;
@@ -29,6 +28,7 @@ use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
@@ -38,12 +38,14 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
@@ -51,6 +53,7 @@ use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr;
@@ -94,88 +97,265 @@ class EditServer extends EditRecord
->label(trans('admin/server.tabs.information'))
->icon('tabler-info-circle')
->schema([
TextInput::make('name')
->prefixIcon('tabler-server')
->label(trans('admin/server.name'))
->suffixAction(Action::make('random')
->icon('tabler-dice-' . random_int(1, 6))
->action(function (Set $set, Get $get) {
$egg = Egg::find($get('egg_id'));
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
$word = (new RandomWordService())->word();
$set('name', $prefix . $word);
}))
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 2,
'lg' => 3,
])
->required()
->maxLength(255),
Select::make('owner_id')
->prefixIcon('tabler-user')
->label(trans('admin/server.owner'))
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 2,
'lg' => 2,
])
->relationship('user', 'username')
->searchable(['username', 'email'])
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)")
->preload()
->required(),
ToggleButtons::make('condition')
->label(trans('admin/server.server_status'))
->formatStateUsing(fn (Server $server) => $server->condition)
->options(fn ($state) => [$state->value => $state->getLabel()])
->colors(fn ($state) => [$state->value => $state->getColor()])
->icons(fn ($state) => [$state->value => $state->getIcon()])
->stateCast(new ServerConditionStateCast())
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->hintAction(
Action::make('view_install_log')
->label(trans('admin/server.view_install_log'))
//->visible(fn (Server $server) => $server->isFailedInstall())
->modalHeading('')
->modalSubmitAction(false)
->modalFooterActionsAlignment(Alignment::Right)
->modalCancelActionLabel(trans('filament::components/modal.actions.close.label'))
Grid::make()
->columns(2)
->columnStart(1)
->schema([
Image::make('', 'icon')
->hidden(fn ($record) => !$record->icon && !$record->egg->image)
->url(fn ($record) => $record->icon ?: $record->egg->image)
->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
->columnSpan(2)
->alignJustify(),
Action::make('uploadIcon')
->iconButton()->iconSize(IconSize::Large)
->icon('tabler-photo-up')
->modal()
->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
->schema([
CodeEditor::make('logs')
->hiddenLabel()
->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) {
try {
return $serverRepository->setServer($server)->getInstallLogs();
} catch (ConnectionException) {
Notification::make()
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.log_failed'))
->color('warning')
->warning()
->send();
} catch (Exception) {
return '';
}
Tabs::make()->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('base64Image'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
return '';
}),
return;
}
try {
if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
$allowedExtensions = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
];
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, $allowedExtensions)) {
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys($allowedExtensions))]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new \Exception(trans('admin/egg.import.no_local_ip'));
}
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$imageContent = @file_get_contents($state, false, $context, 0, 262144); //256KB
if (!$imageContent) {
throw new \Exception(trans('admin/egg.import.image_error'));
}
$mimeType = $allowedExtensions[$extension];
$base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent);
$set('base64Image', $base64);
$set('image_url_error', null);
} catch (\Exception $e) {
$set('image_url_error', $e->getMessage());
$set('base64Image', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn (Get $get) => $get('image_url_error') !== null)
->afterStateHydrated(fn (Get $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->saveUploadedFileUsing(function ($file, Set $set) {
$base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath()));
$set('base64Image', $base64);
return $base64;
}),
]),
]),
])
),
->action(function (array $data, $record): void {
$base64 = $data['base64Image'] ?? null;
if (empty($base64) && !empty($data['image'])) {
$base64 = $data['image'];
}
if (!empty($base64)) {
$record->update([
'icon' => $base64,
]);
Notification::make()
->title(trans('server/setting.server_info.icon.updated'))
->success()
->send();
$record->refresh();
} else {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('deleteIcon')
->visible(fn ($record) => $record->icon)
->label('')
->icon('tabler-trash')
->iconButton()->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
$record->update([
'icon' => null,
]);
Notification::make()
->title(trans('server/setting.server_info.icon.deleted'))
->success()
->send();
$record->refresh();
}),
]),
Grid::make()
->columns(3)
->columnStart(2)
->columnSpan([
'default' => 2,
'sm' => 2,
'md' => 3,
'lg' => 5,
])
->schema([
TextInput::make('name')
->prefixIcon('tabler-server')
->label(trans('admin/server.name'))
->suffixAction(Action::make('random')
->icon('tabler-dice-' . random_int(1, 6))
->action(function (Set $set, Get $get) {
$egg = Egg::find($get('egg_id'));
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
$word = (new RandomWordService())->word();
$set('name', $prefix . $word);
}))
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 2,
'lg' => 3,
])
->required()
->maxLength(255),
Select::make('owner_id')
->prefixIcon('tabler-user')
->label(trans('admin/server.owner'))
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 2,
'lg' => 2,
])
->relationship('user', 'username')
->searchable(['username', 'email'])
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)")
->preload()
->required(),
ToggleButtons::make('condition')
->label(trans('admin/server.server_status'))
->formatStateUsing(fn (Server $server) => $server->condition)
->options(fn ($state) => [$state->value => $state->getLabel()])
->colors(fn ($state) => [$state->value => $state->getColor()])
->icons(fn ($state) => [$state->value => $state->getIcon()])
->stateCast(new ServerConditionStateCast())
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 1,
'lg' => 1,
])
->hintAction(
Action::make('view_install_log')
->label(trans('admin/server.view_install_log'))
//->visible(fn (Server $server) => $server->isFailedInstall())
->modalHeading('')
->modalSubmitAction(false)
->modalFooterActionsAlignment(Alignment::Right)
->modalCancelActionLabel(trans('filament::components/modal.actions.close.label'))
->schema([
CodeEditor::make('logs')
->hiddenLabel()
->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) {
try {
$logs = $serverRepository->setServer($server)->getInstallLogs();
return mb_convert_encoding($logs, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'Windows-1252', 'ASCII']);
} catch (ConnectionException) {
Notification::make()
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.log_failed'))
->color('warning')
->warning()
->send();
} catch (Exception) {
return '';
}
return '';
}),
])
),
]),
Textarea::make('description')
->label(trans('admin/server.description'))
->columnSpanFull(),
TextInput::make('uuid')
->label(trans('admin/server.uuid'))
->copyable()
@@ -616,6 +796,7 @@ class EditServer extends EditRecord
Select::make('select_startup')
->label(trans('admin/server.startup_cmd'))
->required()
->live()
->afterStateUpdated(function (Set $set, $state) {
$set('startup', $state);
@@ -632,7 +813,22 @@ class EditServer extends EditRecord
$set('select_startup', $currentStartup);
}
return array_flip($startups) + ['' => 'Custom Startup'];
return array_flip($startups) + ['custom' => 'Custom Startup'];
})
->formatStateUsing(function (Server $server) {
$startups = $server->egg->startup_commands;
$currentStartup = $server->startup;
$matchingStartup = collect($startups)
->filter(fn ($value, $key) => $value === $currentStartup)
->keys()
->first();
if (!$matchingStartup) {
return 'custom';
}
return $matchingStartup;
})
->selectablePlaceholder(false)
->columnSpanFull()
@@ -650,7 +846,7 @@ class EditServer extends EditRecord
if (in_array($state, $startups)) {
$set('select_startup', $state);
} else {
$set('select_startup', '');
$set('select_startup', 'custom');
}
})
->placeholder(trans('admin/server.startup_placeholder'))
@@ -811,7 +1007,7 @@ class EditServer extends EditRecord
Actions::make([
Action::make('transfer')
->label(trans('admin/server.transfer'))
->disabled(fn (Server $server) => Node::count() <= 1 || $server->isInConflictState())
->disabled(fn (Server $server) => user()?->accessibleNodes()->count() <= 1 || $server->isInConflictState())
->modalHeading(trans('admin/server.transfer'))
->schema($this->transferServer())
->action(function (TransferServerService $transfer, Server $server, $data) {
@@ -883,10 +1079,10 @@ class EditServer extends EditRecord
->label(trans('admin/server.node'))
->prefixIcon('tabler-server-2')
->selectablePlaceholder(false)
->default(fn (Server $server) => Node::whereNot('id', $server->node->id)->first()?->id)
->default(fn (Server $server) => user()?->accessibleNodes()->whereNot('id', $server->node->id)->first()?->id)
->required()
->live()
->options(fn (Server $server) => Node::whereNot('id', $server->node->id)->pluck('name', 'id')->all()),
->options(fn (Server $server) => user()?->accessibleNodes()->whereNot('id', $server->node->id)->pluck('name', 'id')->all()),
Select::make('allocation_id')
->label(trans('admin/server.primary_allocation'))
->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id)
@@ -942,7 +1138,9 @@ class EditServer extends EditRecord
}
})
->hidden(fn () => $canForceDelete)
->authorize(fn (Server $server) => user()?->can('delete server', $server)),
->authorize(fn (Server $server) => user()?->can('delete server', $server))
->icon('tabler-trash')
->iconButton()->iconSize(IconSize::ExtraLarge),
Action::make('ForceDelete')
->color('danger')
->label(trans('filament-actions::force-delete.single.label'))
@@ -963,8 +1161,11 @@ class EditServer extends EditRecord
Action::make('console')
->label(trans('admin/server.console'))
->icon('tabler-terminal')
->iconButton()->iconSize(IconSize::ExtraLarge)
->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)),
$this->getSaveFormAction()->formId('form'),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}

View File

@@ -12,10 +12,12 @@ use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class ListServers extends ListRecords
{
@@ -47,7 +49,9 @@ class ListServers extends ListRecords
->searchable(),
TextColumn::make('name')
->label(trans('admin/server.name'))
->searchable()
->searchable(query: fn (Builder $query, string $search) => $query->where(
Server::query()->qualifyColumn('name'), 'like', "%{$search}%")
)
->sortable(),
TextColumn::make('node.name')
->label(trans('admin/server.node'))
@@ -89,6 +93,9 @@ class ListServers extends ListRecords
->recordActions([
Action::make('View')
->label(trans('admin/server.view'))
->iconButton()
->icon('tabler-terminal')
->iconSize(IconSize::Large)
->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
->authorize(fn (Server $server) => user()?->canAccessTenant($server)),
EditAction::make(),
@@ -96,10 +103,7 @@ class ListServers extends ListRecords
->emptyStateIcon('tabler-brand-docker')
->searchable()
->emptyStateDescription('')
->emptyStateHeading(trans('admin/server.no_servers'))
->emptyStateActions([
CreateAction::make(),
]);
->emptyStateHeading(trans('admin/server.no_servers'));
}
/** @return array<Action|ActionGroup> */
@@ -107,7 +111,8 @@ class ListServers extends ListRecords
{
return [
CreateAction::make()
->hidden(fn () => Server::count() <= 0),
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -11,12 +11,14 @@ use Filament\Actions\AssociateAction;
use Filament\Actions\CreateAction;
use Filament\Actions\DissociateAction;
use Filament\Actions\DissociateBulkAction;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
@@ -32,11 +34,11 @@ class AllocationsRelationManager extends RelationManager
public function table(Table $table): Table
{
return $table
->heading('')
->selectCurrentPageOnly()
->recordTitleAttribute('address')
->recordTitle(fn (Allocation $allocation) => $allocation->address)
->inverseRelationship('server')
->heading(trans('admin/server.allocations'))
->columns([
TextColumn::make('ip')
->label(trans('admin/server.ip_address')),
@@ -60,20 +62,53 @@ class AllocationsRelationManager extends RelationManager
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
->label(trans('admin/server.primary')),
IconColumn::make('is_locked')
->label(trans('admin/server.locked'))
->tooltip(trans('admin/server.locked_helper'))
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->recordActions([
Action::make('make-primary')
->label(trans('admin/server.make_primary'))
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
Action::make('lock')
->label(trans('admin/server.lock'))
->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => true]) && $this->deselectAllTableRecords())
->hidden(fn (Allocation $allocation) => $allocation->is_locked),
Action::make('unlock')
->label(trans('admin/server.unlock'))
->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => false]) && $this->deselectAllTableRecords())
->visible(fn (Allocation $allocation) => $allocation->is_locked),
DissociateAction::make()
->after(function (Allocation $allocation) {
$allocation->update(['notes' => null]);
$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
$allocation->update([
'notes' => null,
'is_locked' => false,
]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
}
}),
])
->headerActions([
CreateAction::make()->label(trans('admin/server.create_allocation'))
->toolbarActions([
DissociateBulkAction::make()
->after(function () {
Allocation::whereNull('server_id')->update([
'notes' => null,
'is_locked' => false,
]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
}
}),
CreateAction::make()
->label(trans('admin/server.create_allocation'))
->icon('tabler-network')
->iconButton()->iconSize(IconSize::ExtraLarge)
->createAnother(false)
->schema(fn () => [
Select::make('allocation_ip')
@@ -107,22 +142,25 @@ class AllocationsRelationManager extends RelationManager
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))))
->splitKeys(['Tab', ' ', ','])
->required(),
Hidden::make('is_locked')
->default(true),
])
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
AssociateAction::make()
->icon('tabler-file-plus')
->iconButton()->iconSize(IconSize::ExtraLarge)
->multiple()
->associateAnother(false)
->preloadRecordSelect()
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
->recordSelectSearchColumns(['ip', 'port'])
->label(trans('admin/server.add_allocation'))
->after(fn (array $data) => !$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]])),
])
->groupedBulkActions([
DissociateBulkAction::make()
->after(function () {
Allocation::whereNull('server_id')->update(['notes' => null]);
$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
->after(function (array $data) {
Allocation::whereIn('id', array_values(array_unique($data['recordId'])))->update(['is_locked' => true]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]]);
}
}),
]);
}

View File

@@ -18,6 +18,7 @@ use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Support\Exceptions\Halt;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -61,6 +62,7 @@ class DatabasesRelationManager extends RelationManager
public function table(Table $table): Table
{
return $table
->heading('')
->recordTitleAttribute('database')
->columns([
TextColumn::make('database'),
@@ -81,6 +83,8 @@ class DatabasesRelationManager extends RelationManager
ViewAction::make()
->color('primary'),
DeleteAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge)
->successNotificationTitle(null)
->using(function (Database $database, DatabaseManagementService $service) {
try {
$service->delete($database);
@@ -99,11 +103,13 @@ class DatabasesRelationManager extends RelationManager
}
}),
])
->headerActions([
->toolbarActions([
CreateAction::make()
->disabled(fn () => DatabaseHost::count() < 1)
->label(fn () => DatabaseHost::count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database'))
->color(fn () => DatabaseHost::count() < 1 ? 'danger' : 'primary')
->icon(fn () => DatabaseHost::count() < 1 ? 'tabler-database-x' : 'tabler-database-plus')
->iconButton()->iconSize(IconSize::ExtraLarge)
->createAnother(false)
->action(function (array $data, DatabaseManagementService $service, RandomWordService $randomWordService) {
$data['database'] ??= $randomWordService->word() . random_int(1, 420);

View File

@@ -10,6 +10,7 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Model;
class CreateUser extends CreateRecord
@@ -32,7 +33,9 @@ class CreateUser extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-user-plus'),
];
}

View File

@@ -11,6 +11,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Model;
class EditUser extends EditRecord
@@ -33,8 +34,11 @@ class EditUser extends EditRecord
return [
DeleteAction::make()
->label(fn (User $user) => user()?->id === $user->id ? trans('admin/user.self_delete') : ($user->servers()->count() > 0 ? trans('admin/user.has_servers') : trans('filament-actions::delete.single.modal.actions.delete.label')))
->disabled(fn (User $user) => user()?->id === $user->id || $user->servers()->count() > 0),
$this->getSaveFormAction()->formId('form'),
->disabled(fn (User $user) => user()?->id === $user->id || $user->servers()->count() > 0)
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}

View File

@@ -9,6 +9,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListUsers extends ListRecords
{
@@ -21,7 +22,9 @@ class ListUsers extends ListRecords
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make(),
CreateAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-user-plus'),
];
}
}

View File

@@ -9,6 +9,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewUser extends ViewRecord
{
@@ -21,7 +22,8 @@ class ViewUser extends ViewRecord
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),
EditAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}
}

View File

@@ -130,7 +130,7 @@ class UserResource extends Resource
])
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::canEdit($record)),
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
])
->checkIfRecordIsSelectableUsing(fn (User $user) => user()?->id !== $user->id && !$user->servers_count)
@@ -188,7 +188,7 @@ class UserResource extends Resource
->hintAction(
Action::make('password_reset')
->label(trans('admin/user.password_reset'))
->hidden(fn () => config('mail.default', 'log') === 'log')
->hidden(fn (string $operation) => $operation === 'create' || config('mail.default', 'log') === 'log')
->icon('tabler-send')
->action(function (User $user) {
$status = Password::broker(Filament::getPanel('app')->getAuthPasswordBroker())->sendResetLink([
@@ -236,8 +236,7 @@ class UserResource extends Resource
->default(fn () => config('app.timezone', 'UTC'))
->selectablePlaceholder(false)
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
->searchable()
->native(false),
->searchable(),
Select::make('language')
->label(trans('profile.language'))
->columnSpan([
@@ -251,8 +250,7 @@ class UserResource extends Resource
->default('en')
->searchable()
->selectablePlaceholder(false)
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->native(false),
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
FileUpload::make('avatar')
->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false)
->avatar()
@@ -414,7 +412,7 @@ class UserResource extends Resource
$sshKey->delete();
Activity::event('user:ssh-key.delete')
->actor(auth()->user())
->actor(user())
->subject($user)
->subject($sshKey)
->property('fingerprint', $sshKey->fingerprint)

View File

@@ -9,6 +9,7 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\IconSize;
class CreateWebhookConfiguration extends CreateRecord
{
@@ -23,8 +24,10 @@ class CreateWebhookConfiguration extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCancelFormAction()->formId('form'),
$this->getCreateFormAction()->formId('form'),
$this->getCancelFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}

View File

@@ -11,6 +11,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
class EditWebhookConfiguration extends EditRecord
{
@@ -23,14 +24,17 @@ class EditWebhookConfiguration extends EditRecord
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make(),
DeleteAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
Action::make('test_now')
->label(trans('admin/webhook.test_now'))
->color('primary')
->disabled(fn (WebhookConfiguration $webhookConfiguration) => count($webhookConfiguration->events) === 0)
->action(fn (WebhookConfiguration $webhookConfiguration) => $webhookConfiguration->run())
->tooltip(trans('admin/webhook.test_now_help')),
$this->getSaveFormAction()->formId('form'),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}

View File

@@ -3,13 +3,13 @@
namespace App\Filament\Admin\Resources\Webhooks\Pages;
use App\Filament\Admin\Resources\Webhooks\WebhookResource;
use App\Models\WebhookConfiguration;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListWebhookConfigurations extends ListRecords
{
@@ -23,7 +23,8 @@ class ListWebhookConfigurations extends ListRecords
{
return [
CreateAction::make()
->hidden(fn () => WebhookConfiguration::count() <= 0),
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -9,6 +9,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewWebhookConfiguration extends ViewRecord
{
@@ -21,7 +22,8 @@ class ViewWebhookConfiguration extends ViewRecord
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),
EditAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}
}

View File

@@ -15,7 +15,6 @@ use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ReplicateAction;
@@ -98,7 +97,7 @@ class WebhookResource extends Resource
])
->recordActions([
ViewAction::make()
->hidden(fn (WebhookConfiguration $record) => static::canEdit($record)),
->hidden(fn (WebhookConfiguration $record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
ReplicateAction::make()
->iconButton()
@@ -114,9 +113,6 @@ class WebhookResource extends Resource
->emptyStateIcon('tabler-webhook')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/webhook.no_webhooks'))
->emptyStateActions([
CreateAction::make(),
])
->persistFiltersInSession()
->filters([
SelectFilter::make('type')

View File

@@ -5,6 +5,7 @@ namespace App\Filament\App\Resources\Servers\Pages;
use App\Enums\CustomizationKey;
use App\Enums\ServerResourceType;
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\Permission;
@@ -20,6 +21,7 @@ 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;
@@ -33,11 +35,11 @@ class ListServers extends ListRecords
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ServerResource::class;
public const WARNING_THRESHOLD = 0.7;
public const DANGER_THRESHOLD = 0.9;
public const WARNING_THRESHOLD = 0.7;
protected static string $resource = ServerResource::class;
private DaemonServerRepository $daemonServerRepository;
@@ -53,6 +55,8 @@ class ListServers extends ListRecords
return [
Stack::make([
ServerEntryColumn::make('server_entry')
->warningThresholdPercent(static::WARNING_THRESHOLD)
->dangerThresholdPercent(static::DANGER_THRESHOLD)
->searchable(['name']),
]),
];
@@ -62,10 +66,14 @@ class ListServers extends ListRecords
protected function tableColumns(): array
{
return [
ImageColumn::make('icon')
->label('')
->imageSize(46)
->state(fn (Server $server) => $server->icon ?: $server->egg->image),
TextColumn::make('condition')
->label(trans('server/dashboard.status'))
->badge()
->tooltip(fn (Server $server) => $server->formatResource(ServerResourceType::Uptime))
->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')
@@ -79,24 +87,27 @@ class ListServers extends ListRecords
->visibleFrom('md')
->copyable()
->state(fn (Server $server) => $server->allocation->address ?? 'None'),
TextColumn::make('cpuUsage')
->label(trans('server/dashboard.resources'))
->icon('tabler-cpu')
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::CPULimit)]))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::CPU))
->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
TextColumn::make('memoryUsage')
ProgressBarColumn::make('cpuUsage')
->label('')
->icon('tabler-device-desktop-analytics')
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::MemoryLimit)]))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::Memory))
->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
TextColumn::make('diskUsage')
->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('')
->icon('tabler-device-sd-card')
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::DiskLimit)]))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::Disk))
->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
->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)),
];
}
@@ -107,7 +118,8 @@ class ListServers extends ListRecords
$usingGrid = user()?->getCustomization(CustomizationKey::DashboardLayout) === 'grid';
return $table
->paginated(false)
->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())

View File

@@ -10,10 +10,17 @@ class ServerResource extends Resource
{
protected static ?string $model = Server::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker';
protected static ?string $slug = '/';
protected static bool $shouldRegisterNavigation = false;
public static function getNavigationBadge(): ?string
{
return (string) user()?->directAccessibleServers()->where('owner_id', user()?->id)->count();
}
public static function canAccess(): bool
{
return true;
@@ -25,4 +32,10 @@ class ServerResource extends Resource
'index' => ListServers::route('/'),
];
}
public static function embedServerList(bool $condition = true): void
{
static::$slug = $condition ? null : '/';
static::$shouldRegisterNavigation = $condition;
}
}

View File

@@ -7,6 +7,7 @@ use App\Models\Egg;
use Filament\Actions\Action;
use Filament\Infolists\Components\TextEntry;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
class ExportEggAction extends Action
{
@@ -21,8 +22,14 @@ class ExportEggAction extends Action
$this->label(trans('filament-actions::export.modal.actions.export.label'));
$this->iconButton();
$this->icon('tabler-file-export');
$this->tableIcon('tabler-download');
$this->iconSize(IconSize::ExtraLarge);
$this->authorize(fn () => user()?->can('export egg'));
$this->modalHeading(fn (Egg $egg) => trans('filament-actions::export.modal.actions.export.label') . ' ' . $egg->name);

View File

@@ -8,6 +8,7 @@ use App\Models\Server;
use App\Services\Schedules\Sharing\ScheduleExporterService;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Support\Enums\IconSize;
class ExportScheduleAction extends Action
{
@@ -20,6 +21,16 @@ class ExportScheduleAction extends Action
{
parent::setUp();
$this->hiddenLabel();
$this->iconButton();
$this->iconSize(IconSize::ExtraLarge);
$this->icon('tabler-download');
$this->tooltip(trans('server/schedule.export'));
/** @var Server $server */
$server = Filament::getTenant();

View File

@@ -17,6 +17,7 @@ use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Str;
@@ -35,6 +36,12 @@ class ImportEggAction extends Action
$this->label(trans('filament-actions::import.modal.actions.import.label'));
$this->iconButton();
$this->icon('tabler-file-import');
$this->iconSize(IconSize::ExtraLarge);
$this->authorize(fn () => user()?->can('import egg'));
$this->action(function (array $data, EggImporterService $eggImportService): void {

View File

@@ -7,6 +7,7 @@ use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
class UpdateEggAction extends Action
{
@@ -21,8 +22,12 @@ class UpdateEggAction extends Action
$this->label(trans_choice('admin/egg.update', 1));
$this->iconButton();
$this->icon('tabler-cloud-download');
$this->iconSize(IconSize::ExtraLarge);
$this->color('success');
$this->requiresConfirmation();

View File

@@ -49,9 +49,15 @@ class UpdateEggBulkAction extends BulkAction
$success = 0;
$failed = 0;
$skipped = 0;
/** @var Egg $egg */
foreach ($records as $egg) {
if ($egg->update_url === null) {
$skipped++;
continue;
}
try {
$eggImporterService->fromUrl($egg->update_url, $egg);
@@ -67,7 +73,12 @@ class UpdateEggBulkAction extends BulkAction
Notification::make()
->title(trans_choice('admin/egg.updated', 2, ['count' => $success, 'total' => $records->count()]))
->body($failed > 0 ? trans('admin/egg.updated_failed', ['count' => $failed]) : null)
->body(
collect([
$failed > 0 ? trans('admin/egg.updated_failed', ['count' => $failed]) : null,
$skipped > 0 ? trans('admin/egg.updated_skipped', ['count' => $skipped]) : null,
])->filter()->join(' ')
)
->status($failed > 0 ? 'warning' : 'success')
->persistent()
->send();

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Filament\Components\Actions;
use App\Models\Allocation;
use App\Models\Node;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
class UpdateNodeAllocations extends Action
{
public static function getDefaultName(): ?string
{
return 'bulk_update_ip';
}
protected function setUp(): void
{
parent::setUp();
$this->label(trans('admin/node.bulk_update_ip'));
$this->icon('tabler-replace');
$this->iconSize(IconSize::ExtraLarge);
$this->iconButton();
$this->color('warning');
$this->requiresConfirmation();
$this->modalHeading(trans('admin/node.bulk_update_ip'));
$this->modalDescription(trans('admin/node.bulk_update_ip_description'));
$this->modalIconColor('warning');
$this->modalSubmitActionLabel(trans('admin/node.update_ip'));
$this->schema(function () {
/** @var Node $node */
$node = $this->record;
$currentIps = Allocation::where('node_id', $node->id)
->pluck('ip')
->unique()
->values()
->all();
return [
Select::make('old_ip')
->label(trans('admin/node.old_ip'))
->options(array_combine($currentIps, $currentIps))
->selectablePlaceholder(false)
->required()
->live(),
Select::make('new_ip')
->label(trans('admin/node.new_ip'))
->options(fn () => array_combine($node->ipAddresses(), $node->ipAddresses()) ?: [])
->required()
->different('old_ip'),
];
});
$this->action(function (array $data) {
/** @var Node $node */
$node = $this->record;
$allocations = Allocation::where('node_id', $node->id)->where('ip', $data['old_ip'])->get();
if ($allocations->count() === 0) {
Notification::make()
->title(trans('admin/node.no_allocations_to_update'))
->warning()
->send();
return;
}
$updated = 0;
$failed = 0;
foreach ($allocations as $allocation) {
try {
$allocation->update(['ip' => $data['new_ip']]);
$updated++;
} catch (Exception $exception) {
$failed++;
report($exception);
}
}
Notification::make()
->title(trans('admin/node.ip_updated', ['count' => $updated, 'total' => $allocations->count()]))
->body($failed > 0 ? trans('admin/node.ip_update_failed', ['count' => $failed]) : null)
->status($failed > 0 ? 'warning' : 'success')
->persistent()
->send();
});
}
public function nodeRecord(Node $node): static
{
$this->record = $node;
return $this;
}
}

View File

@@ -21,8 +21,6 @@ class CopyFrom extends Select
$this->searchable();
$this->native(false);
$this->live();
}
@@ -30,7 +28,7 @@ class CopyFrom extends Select
{
$this->helperText(trans('admin/egg.copy_from_help'));
$this->relationship('configFrom', 'name');
$this->relationship('configFrom', 'name', ignoreRecord: true);
$this->afterStateUpdated(function ($state, Set $set) {
$set('copy_script_from', $state);
@@ -54,7 +52,7 @@ class CopyFrom extends Select
public function script(): static
{
$this->relationship('scriptFrom', 'name');
$this->relationship('scriptFrom', 'name', ignoreRecord: true);
$this->afterStateUpdated(function ($state, Set $set, Component $livewire) {
if ($state === null) {

View File

@@ -0,0 +1,136 @@
<?php
namespace App\Filament\Components\Tables\Columns\Concerns;
use Closure;
use Filament\Support\Colors\Color;
/**
* Trait extracted for progress-related shared functionality between columns.
*
* @method mixed evaluate(mixed $value, array<string,mixed> $context = [])
*/
trait HasProgress
{
protected float|Closure|null $warningThresholdPercent = null;
protected float|Closure|null $dangerThresholdPercent = null;
/**
* @var string|array<int|string,string>|Closure|Color|null
*/
protected string|array|Closure|Color|null $dangerColor = null;
/**
* @var string|array<int|string,string>|Closure|Color|null
*/
protected string|array|Closure|Color|null $warningColor = null;
/**
* @var string|array<int|string,string>|Closure|Color|null
*/
protected string|array|Closure|Color|null $color = null;
public function warningThresholdPercent(float|Closure $value): static
{
$this->warningThresholdPercent = $value;
return $this;
}
public function getWarningThresholdPercent(): ?float
{
return $this->evaluate($this->warningThresholdPercent);
}
public function dangerThresholdPercent(float|Closure $value): static
{
$this->dangerThresholdPercent = $value;
return $this;
}
public function getDangerThresholdPercent(): ?float
{
return $this->evaluate($this->dangerThresholdPercent);
}
/**
* @param string|array<int|string,string>|Closure|Color $color
*/
public function dangerColor(string|array|Closure|Color $color): static
{
$this->dangerColor = $color;
return $this;
}
/**
* @return string|array<int|string,string>|Color|null
*/
public function getDangerColor(): string|array|Color|null
{
return $this->evaluate($this->dangerColor);
}
/**
* @param string|array<int|string,string>|Closure|Color $color
*/
public function warningColor(string|array|Closure|Color $color): static
{
$this->warningColor = $color;
return $this;
}
/**
* @return string|array<int|string,string>|Color|null
*/
public function getWarningColor(): string|array|Color|null
{
return $this->evaluate($this->warningColor);
}
/**
* @param string|array<int|string,string>|Closure|Color $color
*/
public function color(string|array|Closure|Color $color): static
{
$this->color = $color;
return $this;
}
/**
* @return string|array<int|string,string>|Color|null
*/
public function getColor(): string|array|Color|null
{
return $this->evaluate($this->color);
}
/**
* Resolve a progress color for a given status string ('danger','warning','success').
*
* @return string|array<int|string,string>|Color
*/
public function getProgressColorForStatus(string $status): string|array|Color
{
$color = match ($status) {
'danger' => $this->getDangerColor(),
'warning' => $this->getWarningColor(),
'success' => $this->getColor(),
default => $this->getColor(),
};
if ($color === null) {
$color = $this->getColor();
}
if ($color === null) {
return 'gray';
}
return $color;
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace App\Filament\Components\Tables\Columns;
use App\Filament\Components\Tables\Columns\Concerns\HasProgress;
use Closure;
use Filament\Support\Facades\FilamentColor;
use Filament\Tables\Columns\Column;
class ProgressBarColumn extends Column
{
use HasProgress;
protected string $view = 'livewire.columns.progress-bar-column';
protected int|float|Closure|null $maxValue = null;
protected string|Closure|null $helperLabel = null;
protected function setUp(): void
{
parent::setUp();
$this->dangerColor = FilamentColor::getColor('danger');
$this->warningColor = FilamentColor::getColor('warning');
$this->color = FilamentColor::getColor('primary');
$this->helperLabel = fn ($state) => $state !== null ? (string) $state : '0';
}
public function maxValue(int|float|Closure $value): static
{
$this->maxValue = $value;
return $this;
}
public function getMaxValue(): ?float
{
return $this->evaluate($this->maxValue);
}
public function helperLabel(string|Closure $label): static
{
$this->helperLabel = $label;
return $this;
}
public function getHelperLabel(mixed $currentValue = null): string
{
$result = $this->evaluate($this->helperLabel, [
'state' => $currentValue,
'percentage' => $this->getProgressPercentage(),
]);
return $result !== null ? (string) $result : '';
}
public function getProgressPercentage(): float
{
$currentValue = $this->getState();
$maxValue = $this->getMaxValue();
if ($currentValue === null || $maxValue === null || $maxValue <= 0) {
return 0;
}
return min(100, max(0, ($currentValue / $maxValue) * 100));
}
public function getProgressStatus(): string
{
$percentage = $this->getProgressPercentage();
$dangerPercent = $this->getDangerThresholdPercent();
$warningPercent = $this->getWarningThresholdPercent();
$dangerThreshold = ($dangerPercent !== null ? $dangerPercent : 0.9) * 100;
$warningThreshold = ($warningPercent !== null ? $warningPercent : 0.7) * 100;
if ($percentage >= $dangerThreshold) {
return 'danger';
}
if ($percentage >= $warningThreshold) {
return 'warning';
}
return 'success';
}
public function getProgressLabel(): string
{
$currentValue = $this->getState();
$label = $this->getHelperLabel($currentValue);
if ($label !== '') {
return $label;
}
return sprintf('%d%%', (int) round($this->getProgressPercentage()));
}
/**
* @return string|array<int|string,string>
*/
public function getProgressColor(): string|array
{
$status = $this->getProgressStatus();
$color = match ($status) {
'danger' => $this->getDangerColor(),
'warning' => $this->getWarningColor(),
'success' => $this->getColor(),
default => $this->getColor(),
};
if ($color === null) {
$color = $this->getColor();
}
if ($color === null) {
return 'gray';
}
return $color;
}
public static function resolveColor(mixed $color): ?string
{
$resolvedColor = null;
if (is_object($color)) {
if (method_exists($color, 'toCss')) {
$resolvedColor = $color->toCss();
} elseif (method_exists($color, 'toRgb')) {
$resolvedColor = $color->toRgb();
} elseif (method_exists($color, 'toHex')) {
$resolvedColor = $color->toHex();
} else {
$resolvedColor = $color;
}
} elseif (is_array($color)) {
$resolvedColor = $color[500] ?? reset($color) ?? null;
} else {
$resolvedColor = (string) $color;
}
if (is_string($resolvedColor)) {
return $resolvedColor;
}
return null;
}
public function getResolvedProgressColor(): ?string
{
return self::resolveColor($this->getProgressColor());
}
}

View File

@@ -2,9 +2,22 @@
namespace App\Filament\Components\Tables\Columns;
use App\Filament\Components\Tables\Columns\Concerns\HasProgress;
use Filament\Support\Facades\FilamentColor;
use Filament\Tables\Columns\Column;
class ServerEntryColumn extends Column
{
use HasProgress;
protected string $view = 'livewire.columns.server-entry-column';
protected function setUp(): void
{
parent::setUp();
$this->dangerColor = FilamentColor::getColor('danger');
$this->warningColor = FilamentColor::getColor('warning');
$this->color = FilamentColor::getColor('primary');
}
}

View File

@@ -34,13 +34,12 @@ use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Colors\Color;
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\Width;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Hash;
@@ -132,8 +131,7 @@ class EditProfile extends BaseEditProfile
->default(config('app.timezone', 'UTC'))
->selectablePlaceholder(false)
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
->searchable()
->native(false),
->searchable(),
Select::make('language')
->label(trans('profile.language'))
->required()
@@ -143,8 +141,7 @@ class EditProfile extends BaseEditProfile
->selectablePlaceholder(false)
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? ''
: trans('profile.language_help', ['state' => $state]) . ' <u><a href="https://crowdin.com/project/pelican-dev/">Update On Crowdin</a></u>'))
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->native(false),
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
FileUpload::make('avatar')
->visible(fn () => config('panel.filament.uploadable-avatars'))
->avatar()
@@ -442,10 +439,10 @@ class EditProfile extends BaseEditProfile
->label(trans('profile.navigation'))
->inline()
->options([
1 => trans('profile.top'),
0 => trans('profile.side'),
])
->stateCast(new BooleanStateCast(false, true)),
'sidebar' => trans('profile.sidebar'),
'topbar' => trans('profile.topbar'),
'mixed' => trans('profile.mixed'),
]),
]),
Section::make(trans('profile.console'))
->collapsible()
@@ -458,6 +455,7 @@ class EditProfile extends BaseEditProfile
->minValue(1)
->numeric()
->required()
->live()
->default(14),
Select::make('console_font')
->label(trans('profile.font'))
@@ -482,9 +480,8 @@ class EditProfile extends BaseEditProfile
return $fonts;
})
->reactive()
->default('monospace')
->afterStateUpdated(fn ($state, Set $set) => $set('font_preview', $state)),
->live()
->default('monospace'),
TextEntry::make('font_preview')
->label(trans('profile.font_preview'))
->columnSpan(2)
@@ -554,7 +551,12 @@ class EditProfile extends BaseEditProfile
protected function getDefaultHeaderActions(): array
{
return [
$this->getSaveFormAction()->formId('form'),
$this->getCancelFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-arrow-left'),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
];
}
@@ -584,7 +586,14 @@ class EditProfile extends BaseEditProfile
$data['console_rows'] = (int) $this->getUser()->getCustomization(CustomizationKey::ConsoleRows);
$data['console_graph_period'] = (int) $this->getUser()->getCustomization(CustomizationKey::ConsoleGraphPeriod);
$data['dashboard_layout'] = $this->getUser()->getCustomization(CustomizationKey::DashboardLayout);
$data['top_navigation'] = (bool) $this->getUser()->getCustomization(CustomizationKey::TopNavigation);
// Handle migration from boolean to string navigation types
$topNavigation = $this->getUser()->getCustomization(CustomizationKey::TopNavigation);
if (is_bool($topNavigation)) {
$data['top_navigation'] = $topNavigation ? 'topbar' : 'sidebar';
} else {
$data['top_navigation'] = $topNavigation;
}
return $data;
}

View File

@@ -8,14 +8,23 @@ use App\Models\Server;
use App\Services\Servers\ReinstallServerService;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
class Settings extends ServerFormPage
{
@@ -29,51 +38,208 @@ class Settings extends ServerFormPage
public function form(Schema $schema): Schema
{
return parent::form($schema)
->columns(4)
->components([
Section::make(trans('server/setting.server_info.title'))
->columnSpanFull()
->columns([
'default' => 1,
'sm' => 2,
'sm' => 1,
'md' => 4,
'lg' => 6,
])
->schema([
Fieldset::make()
->label(trans('server/setting.server_info.information'))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 6,
])
->columnSpanFull()
->schema([
TextInput::make('name')
->label(trans('server/setting.server_info.name'))
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_RENAME, $server))
->required()
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 6,
])
->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)),
Textarea::make('description')
->label(trans('server/setting.server_info.description'))
->hidden(!config('panel.editable_server_descriptions'))
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_RENAME, $server))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 6,
])
->autosize()
->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
Grid::make()
->columns(2)
->columnSpan(5)
->schema([
TextInput::make('name')
->columnStart(1)
->columnSpanFull()
->label(trans('server/setting.server_info.name'))
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_RENAME, $server))
->required()
->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)),
Textarea::make('description')
->columnStart(1)
->columnSpanFull()
->label(trans('server/setting.server_info.description'))
->hidden(!config('panel.editable_server_descriptions'))
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_DESCRIPTION, $server))
->autosize()
->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
]),
Grid::make()
->columns(2)
->columnStart(6)
->schema([
Image::make('', 'icon')
->hidden(fn ($record) => !$record->icon && !$record->egg->image)
->url(fn ($record) => $record->icon ?: $record->egg->image)
->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
->columnSpan(2)
->alignJustify(),
Action::make('uploadIcon')
->iconButton()->iconSize(IconSize::Large)
->icon('tabler-photo-up')
->modal()
->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
->schema([
Tabs::make()->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('base64Image'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
return;
}
try {
if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
$allowedExtensions = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
];
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, $allowedExtensions)) {
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys($allowedExtensions))]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new \Exception(trans('admin/egg.import.no_local_ip'));
}
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$imageContent = @file_get_contents($state, false, $context, 0, 262144); //256KB
if (!$imageContent) {
throw new \Exception(trans('admin/egg.import.image_error'));
}
$mimeType = $allowedExtensions[$extension];
$base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent);
$set('base64Image', $base64);
$set('image_url_error', null);
} catch (\Exception $e) {
$set('image_url_error', $e->getMessage());
$set('base64Image', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn (Get $get) => $get('image_url_error') !== null)
->afterStateHydrated(fn (Get $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->saveUploadedFileUsing(function ($file, Set $set) {
$base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath()));
$set('base64Image', $base64);
return $base64;
}),
]),
]),
])
->action(function (array $data, $record): void {
$base64 = $data['base64Image'] ?? null;
if (empty($base64) && !empty($data['image'])) {
$base64 = $data['image'];
}
if (!empty($base64)) {
$record->update([
'icon' => $base64,
]);
Notification::make()
->title(trans('server/setting.server_info.icon.updated'))
->success()
->send();
$record->refresh();
} else {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('deleteIcon')
->visible(fn ($record) => $record->icon)
->label('')
->icon('tabler-trash')
->iconButton()->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
$record->update([
'icon' => null,
]);
Notification::make()
->title(trans('server/setting.server_info.icon.deleted'))
->success()
->send();
$record->refresh();
}),
]),
TextInput::make('uuid')
->label(trans('server/setting.server_info.uuid'))
->columnSpan([
@@ -97,14 +263,14 @@ class Settings extends ServerFormPage
->label(trans('server/setting.server_info.limits.title'))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'sm' => 1,
'md' => 4,
'lg' => 6,
])
->columns([
'default' => 1,
'sm' => 1,
'md' => 1,
'md' => 2,
'lg' => 3,
])
->schema([
@@ -277,7 +443,7 @@ class Settings extends ServerFormPage
public function updateDescription(string $description, Server $server): void
{
abort_unless(user()?->can(Permission::ACTION_SETTINGS_RENAME, $server) && config('panel.editable_server_descriptions'), 403);
abort_unless(user()?->can(Permission::ACTION_SETTINGS_DESCRIPTION, $server) && config('panel.editable_server_descriptions'), 403);
$original = $server->description;

View File

@@ -51,7 +51,7 @@ class Startup extends ServerFormPage
->label(trans('server/startup.command'))
->live()
->visible(fn (Server $server) => in_array($server->startup, $server->egg->startup_commands))
->disabled(fn (Server $server) => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_STARTUP_UPDATE, $server))
->formatStateUsing(fn (Server $server) => $server->startup)
->afterStateUpdated(function ($state, Server $server, Set $set) {
$original = $server->startup;

View File

@@ -6,7 +6,6 @@ use App\Filament\Admin\Resources\Users\Pages\EditUser;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\Activities\Pages\ListActivities;
use App\Models\ActivityLog;
use App\Models\Permission;
use App\Models\Role;
use App\Models\Server;
use App\Models\User;
@@ -164,11 +163,6 @@ class ActivityResource extends Resource
});
}
public static function canViewAny(): bool
{
return user()?->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{

View File

@@ -23,7 +23,6 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class AllocationResource extends Resource
{
@@ -73,15 +72,21 @@ class AllocationResource extends Resource
->action(fn (Allocation $allocation) => user()?->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server) && $server->update(['allocation_id' => $allocation->id]))
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label(trans('server/network.primary')),
IconColumn::make('is_locked')
->label(trans('server/network.locked'))
->tooltip(trans('server/network.locked_helper'))
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->recordActions([
DetachAction::make()
->visible(fn (Allocation $allocation) => !$allocation->is_locked || user()?->can('update', $allocation->node))
->authorize(fn () => user()?->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label(trans('server/network.delete'))
->icon('tabler-trash')
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
Allocation::where('id', $allocation->id)->update([
'notes' => null,
'is_locked' => false,
'server_id' => null,
]);
@@ -93,12 +98,12 @@ class AllocationResource extends Resource
->after(fn (Allocation $allocation) => $allocation->id === $server->allocation_id && $server->update(['allocation_id' => $server->allocations()->first()?->id])),
])
->toolbarActions([
Action::make('addAllocation')
Action::make('add_allocation')
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'tabler-network-off' : 'tabler-network')
->authorize(fn () => user()?->can(Permission::ACTION_ALLOCATION_CREATE, $server))
->tooltip(fn () => $server->allocations()->count() >= $server->allocation_limit ? trans('server/network.limit') : trans('server/network.add'))
->hidden(fn () => !config('panel.client_features.allocations.enabled'))
->hidden(fn () => !config('panel.client_features.allocations.enabled') || $server->allocation === null)
->disabled(fn () => $server->allocations()->count() >= $server->allocation_limit)
->color(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'danger' : 'primary')
->action(function (FindAssignableAllocationService $service) use ($server) {
@@ -116,26 +121,6 @@ class AllocationResource extends Resource
]);
}
public static function canViewAny(): bool
{
return user()?->can(Permission::ACTION_ALLOCATION_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return user()?->can(Permission::ACTION_ALLOCATION_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return user()?->can(Permission::ACTION_ALLOCATION_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return user()?->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant());
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{

View File

@@ -40,7 +40,6 @@ use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -230,6 +229,7 @@ class BackupResource extends Resource
->disabled(fn (Backup $backup) => $backup->is_locked && $backup->status !== BackupStatus::Failed)
->modalDescription(fn (Backup $backup) => trans('server/backup.actions.delete.description', ['backup' => $backup->name]))
->modalSubmitActionLabel(trans('server/backup.actions.delete.title'))
->successNotificationTitle(null)
->action(function (Backup $backup, DeleteBackupService $deleteBackupService) {
try {
$deleteBackupService->handle($backup);
@@ -265,6 +265,7 @@ class BackupResource extends Resource
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
->createAnother(false)
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->successNotificationTitle(null)
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
$action = $initiateBackupService->setIgnoredFiles(explode(PHP_EOL, $data['ignored'] ?? ''));
@@ -296,21 +297,6 @@ class BackupResource extends Resource
]);
}
public static function canViewAny(): bool
{
return user()?->can(Permission::ACTION_BACKUP_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return user()?->can(Permission::ACTION_BACKUP_CREATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return user()?->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant());
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{

View File

@@ -31,7 +31,6 @@ use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class DatabaseResource extends Resource
@@ -138,6 +137,7 @@ class DatabaseResource extends Resource
ViewAction::make()
->modalHeading(fn (Database $database) => trans('server/database.viewing', ['database' => $database->database])),
DeleteAction::make()
->successNotificationTitle(null)
->using(function (Database $database, DatabaseManagementService $service) {
try {
$service->delete($database);
@@ -164,6 +164,7 @@ class DatabaseResource extends Resource
->disabled(fn () => $server->databases()->count() >= $server->database_limit)
->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary')
->createAnother(false)
->successNotificationTitle(null)
->schema([
Grid::make()
->columns(2)
@@ -208,31 +209,6 @@ class DatabaseResource extends Resource
]);
}
public static function canViewAny(): bool
{
return user()?->can(Permission::ACTION_DATABASE_READ, Filament::getTenant());
}
public static function canView(Model $record): bool
{
return user()?->can(Permission::ACTION_DATABASE_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return user()?->can(Permission::ACTION_DATABASE_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return user()?->can(Permission::ACTION_DATABASE_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return user()?->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant());
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{

View File

@@ -7,14 +7,11 @@ use App\Filament\Server\Resources\Files\Pages\EditFiles;
use App\Filament\Server\Resources\Files\Pages\ListFiles;
use App\Filament\Server\Resources\Files\Pages\SearchFiles;
use App\Models\File;
use App\Models\Permission;
use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Facades\Filament;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
class FileResource extends Resource
{
@@ -30,26 +27,6 @@ class FileResource extends Resource
protected static bool $isScopedToTenant = false;
public static function canViewAny(): bool
{
return user()?->can(Permission::ACTION_FILE_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return user()?->can(Permission::ACTION_FILE_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return user()?->can(Permission::ACTION_FILE_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return user()?->can(Permission::ACTION_FILE_DELETE, Filament::getTenant());
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{

View File

@@ -7,11 +7,13 @@ use App\Exceptions\Repository\FileNotEditableException;
use App\Facades\Activity;
use App\Filament\Server\Resources\Files\FileResource;
use App\Livewire\AlertBanner;
use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Closure;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\CodeEditor;
@@ -169,7 +171,9 @@ class EditFiles extends Page
->language(fn (Get $get) => $get('lang'))
->default(function () {
try {
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
$contents = $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
return mb_convert_encoding($contents, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'Windows-1252', 'ASCII']);
} catch (FileSizeTooLargeException) {
AlertBanner::make('file_too_large')
->title(trans('server/file.alerts.file_too_large.title', ['name' => basename($this->path)]))
@@ -215,13 +219,15 @@ class EditFiles extends Page
$this->previousUrl = url()->previous();
if (str($path)->endsWith('.pelicanignore')) {
AlertBanner::make('.pelicanignore_info')
->title(trans('server/file.alerts.pelicanignore.title'))
->body(trans('server/file.alerts.pelicanignore.body'))
->info()
->closable()
->send();
foreach (File::getSpecialFiles() as $fileName => $data) {
if ($data['check'] instanceof Closure && $data['check']($path)) {
AlertBanner::make($fileName . '_info')
->title($data['title'])
->body($data['body'])
->info()
->closable()
->send();
}
}
}
@@ -261,7 +267,7 @@ class EditFiles extends Page
$previousParts = '';
foreach (explode('/', $this->path) as $part) {
$previousParts = $previousParts . '/' . $part;
$breadcrumbs[self::getUrl(['path' => ltrim($previousParts, '/')])] = $part;
$breadcrumbs[ListFiles::getUrl(['path' => ltrim($previousParts, '/')])] = $part;
}
return $breadcrumbs;

View File

@@ -12,8 +12,10 @@ use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Services\Nodes\NodeJWTService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Carbon\CarbonImmutable;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@@ -25,15 +27,14 @@ use Filament\Actions\EditAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Panel;
use Filament\Resources\Pages\ListRecords;
use Filament\Resources\Pages\PageRegistration;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Support\Enums\IconSize;
use Filament\Support\Facades\FilamentView;
@@ -41,7 +42,7 @@ use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Enums\PaginationMode;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\UploadedFile;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Routing\Route;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Route as RouteFacade;
@@ -54,6 +55,8 @@ class ListFiles extends ListRecords
protected static string $resource = FileResource::class;
protected string $view = 'filament.server.pages.list-files';
#[Locked]
public string $path = '/';
@@ -134,7 +137,6 @@ class ListFiles extends ListRecords
->url(fn (File $file) => self::getUrl(['path' => encode_path(join_paths($this->path, $file->name))])),
EditAction::make('edit')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->icon('tabler-edit')
->visible(fn (File $file) => $file->canEdit())
->url(fn (File $file) => EditFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))])),
ActionGroup::make([
@@ -297,13 +299,26 @@ class ListFiles extends ListRecords
->label(trans('server/file.actions.archive.title'))
->icon('tabler-archive')->iconSize(IconSize::Large)
->schema([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
Grid::make(3)
->schema([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->columnSpan(2),
Select::make('extension')
->label(trans('server/file.actions.archive.extension'))
->selectablePlaceholder(false)
->options([
'tar.gz' => 'tar.gz',
'zip' => 'zip',
'tar.bz2' => 'tar.bz2',
'tar.xz' => 'tar.xz',
])
->columnSpan(1),
]),
])
->action(function ($data, File $file) {
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, [$file->name], $data['name']);
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, [$file->name], $data['name'], $data['extension']);
Activity::event('server:file.compress')
->property('name', $archive['name'])
@@ -343,7 +358,7 @@ class ListFiles extends ListRecords
DeleteAction::make()
->authorize(fn () => user()?->can(Permission::ACTION_FILE_DELETE, $server))
->hiddenLabel()
->icon('tabler-trash')->iconSize(IconSize::Large)
->iconSize(IconSize::Large)
->requiresConfirmation()
->modalHeading(fn (File $file) => trans('filament-actions::delete.single.modal.heading', ['label' => $file->name . ' ' . ($file->is_directory ? 'folder' : 'file')]))
->action(function (File $file) {
@@ -392,15 +407,28 @@ class ListFiles extends ListRecords
BulkAction::make('archive')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_ARCHIVE, $server))
->schema([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
Grid::make(3)
->schema([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->columnSpan(2),
Select::make('extension')
->label(trans('server/file.actions.archive.extension'))
->selectablePlaceholder(false)
->options([
'tar.gz' => 'tar.gz',
'zip' => 'zip',
'tar.bz2' => 'tar.bz2',
'tar.xz' => 'tar.xz',
])
->columnSpan(1),
]),
])
->action(function ($data, Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name'], $data['extension']);
Activity::event('server:file.compress')
->property('name', $archive['name'])
@@ -417,6 +445,7 @@ class ListFiles extends ListRecords
$this->refreshPage();
}),
DeleteBulkAction::make()
->successNotificationTitle(null)
->authorize(fn () => user()?->can(Permission::ACTION_FILE_DELETE, $server))
->action(function (Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
@@ -476,7 +505,7 @@ class ListFiles extends ListRecords
->color('primary')
->action(function ($data) {
try {
$this->getDaemonFileRepository()->createDirectory($data['name'], $this->path);
$this->createFolder($data['name']);
Activity::event('server:file.create-directory')
->property(['directory' => $this->path, 'name' => $data['name']])
@@ -500,58 +529,30 @@ class ListFiles extends ListRecords
->label(trans('server/file.actions.new_folder.folder_name'))
->required(),
]),
Action::make('upload')
Action::make('uploadFile')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_CREATE, $server))
->hiddenLabel()->icon('tabler-upload')->iconButton()->iconSize(IconSize::ExtraLarge)
->tooltip(trans('server/file.actions.upload.title'))
->view('filament.server.pages.file-upload'),
Action::make('uploadURL')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_CREATE, $server))
->hiddenLabel()->icon('tabler-download')->iconButton()->iconSize(IconSize::ExtraLarge)
->tooltip(trans('server/file.actions.upload.from_url'))
->modalHeading(trans('server/file.actions.upload.from_url'))
->color('success')
->action(function ($data) {
if (count($data['files']) > 0 && !isset($data['url'])) {
/** @var UploadedFile $file */
foreach ($data['files'] as $file) {
$this->getDaemonFileRepository()->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
$this->getDaemonFileRepository()->pull($data['url'], $this->path);
Activity::event('server:file.uploaded')
->property('directory', $this->path)
->property('file', $file->getClientOriginalName())
->log();
}
} elseif ($data['url'] !== null) {
$this->getDaemonFileRepository()->pull($data['url'], $this->path);
Activity::event('server:file.pull')
->property('url', $data['url'])
->property('directory', $this->path)
->log();
}
Activity::event('server:file.pull')
->property('url', $data['url'])
->property('directory', $this->path)
->log();
$this->refreshPage();
})
->schema([
Tabs::make()
->contained(false)
->schema([
Tab::make('files')
->label(trans('server/file.actions.upload.from_files'))
->live()
->schema([
FileUpload::make('files')
->storeFiles(false)
->previewable(false)
->preserveFilenames()
->maxSize((int) round($server->node->upload_size * (config('panel.use_binary_prefix') ? 1.048576 * 1024 : 1000)))
->multiple(),
]),
Tab::make('url')
->label(trans('server/file.actions.upload.url'))
->live()
->disabled(fn (Get $get) => count($get('files')) > 0)
->schema([
TextInput::make('url')
->label(trans('server/file.actions.upload.url'))
->url(),
]),
]),
TextInput::make('url')
->label(trans('server/file.actions.upload.url'))
->required()
->url(),
]),
Action::make('search')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_READ, $server))
@@ -599,6 +600,81 @@ class ListFiles extends ListRecords
};
}
public function getUploadUrl(NodeJWTService $jwtService): string
{
/** @var Server $server */
$server = Filament::getTenant();
if (!user()?->can(Permission::ACTION_FILE_CREATE, $server)) {
abort(403, 'You do not have permission to upload files.');
}
$token = $jwtService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setUser(user())
->setClaims(['server_uuid' => $server->uuid])
->handle($server->node, user()->id . $server->uuid);
return sprintf(
'%s/upload/file?token=%s',
$server->node->getConnectionAddress(),
$token->toString()
);
}
public function getUploadSizeLimit(): int
{
/** @var Server $server */
$server = Filament::getTenant();
return $server->node->upload_size * 1024 * 1024;
}
/**
* @throws ConnectionException
* @throws FileExistsException
* @throws \Throwable
*/
public function createFolder(string $folderPath): void
{
/** @var Server $server */
$server = Filament::getTenant();
if (!user()?->can(Permission::ACTION_FILE_CREATE, $server)) {
abort(403, 'You do not have permission to create folders.');
}
try {
$this->getDaemonFileRepository()->createDirectory($folderPath, $this->path);
Activity::event('server:file.create-directory')
->property(['directory' => $this->path, 'name' => $folderPath])
->log();
} catch (FileExistsException) {
// Ignore if the folder already exists.
} catch (ConnectionException $e) {
Notification::make()
->body($e->getMessage())
->danger()
->send();
}
}
/**
* @param string[] $files
*/
public function logUploadedFiles(array $files): void
{
$filesCollection = collect($files);
Activity::event('server:files.uploaded')
->property('directory', $this->path)
->property('files', $filesCollection)
->log();
}
private function getDaemonFileRepository(): DaemonFileRepository
{
/** @var Server $server */

View File

@@ -50,24 +50,20 @@ class EditSchedule extends EditRecord
return [
DeleteAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-trash')
->tooltip(trans('server/schedule.delete'))
->after(function ($record) {
Activity::event('server:schedule.delete')
->property('name', $record->name)
->log();
}),
ExportScheduleAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-download')
->tooltip(trans('server/schedule.export')),
ExportScheduleAction::make(),
$this->getSaveFormAction()->formId('form')
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy')
->tooltip(trans('server/schedule.save')),
$this->getCancelFormAction()->formId('form')
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-cancel')
->icon('tabler-arrow-left')
->tooltip(trans('server/schedule.cancel')),
];
}

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Server\Resources\Schedules\RelationManagers;
use App\Extensions\Tasks\TaskService;
use App\Facades\Activity;
use App\Models\Schedule;
use App\Models\Task;
@@ -9,12 +10,12 @@ use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Tables\Columns\IconColumn;
@@ -26,53 +27,31 @@ class TasksRelationManager extends RelationManager
protected static string $relationship = 'tasks';
/**
* @return array<array-key, string>
*/
private function getActionOptions(bool $full = true): array
{
return [
Task::ACTION_POWER => $full ? trans('server/schedule.tasks.actions.power.title') : trans('server/schedule.tasks.actions.power.action'),
Task::ACTION_COMMAND => $full ? trans('server/schedule.tasks.actions.command.title') : trans('server/schedule.tasks.actions.command.command'),
Task::ACTION_BACKUP => $full ? trans('server/schedule.tasks.actions.backup.title') : trans('server/schedule.tasks.actions.backup.files_to_ignore'),
Task::ACTION_DELETE_FILES => $full ? trans('server/schedule.tasks.actions.delete.title') : trans('server/schedule.tasks.actions.delete.files_to_delete'),
];
}
/**
* @return array<Field>
* @return Component[]
*
* @throws Exception
*/
private function getTaskForm(Schedule $schedule): array
{
/** @var TaskService $taskService */
$taskService = app(TaskService::class); // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
$tasks = $taskService->getAll();
return [
Select::make('action')
->label(trans('server/schedule.tasks.actions.title'))
->required()
->live()
->disableOptionWhen(fn (string $value) => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0)
->options($this->getActionOptions())
->disableOptionWhen(fn (string $value) => !$tasks[$value]->canCreate($schedule))
->options($taskService->getMappings())
->selectablePlaceholder(false)
->default(Task::ACTION_POWER)
->afterStateUpdated(fn ($state, Set $set) => $set('payload', $state === Task::ACTION_POWER ? 'restart' : null)),
Textarea::make('payload')
->hidden(fn (Get $get) => $get('action') === Task::ACTION_POWER)
->label(fn (Get $get) => $this->getActionOptions(false)[$get('action')] ?? trans('server/schedule.tasks.payload')),
Select::make('payload')
->visible(fn (Get $get) => $get('action') === Task::ACTION_POWER)
->label(trans('server/schedule.tasks.actions.power.action'))
->required()
->options([
'start' => trans('server/schedule.tasks.actions.power.start'),
'restart' => trans('server/schedule.tasks.actions.power.restart'),
'stop' => trans('server/schedule.tasks.actions.power.stop'),
'kill' => trans('server/schedule.tasks.actions.power.kill'),
])
->selectablePlaceholder(false)
->default('restart'),
->default(array_key_first($tasks))
->afterStateUpdated(fn ($state, Set $set) => $set('payload', $tasks[$state]->getDefaultPayload())),
Group::make(fn (Get $get) => array_key_exists($get('action'), $tasks) ? $tasks[$get('action')]->getPayloadForm() : []),
TextInput::make('time_offset')
->label(trans('server/schedule.tasks.time_offset'))
->hidden(fn (Get $get) => config('queue.default') === 'sync' || $get('sequence_id') === 1 || $schedule->tasks->isEmpty())
->hidden(fn (Get $get, ?Task $task) => config('queue.default') === 'sync' || $schedule->tasks->isEmpty() || $task?->isFirst())
->default(0)
->numeric()
->minValue(0)
@@ -97,19 +76,18 @@ class TasksRelationManager extends RelationManager
->columns([
TextColumn::make('action')
->label(trans('server/schedule.tasks.actions.title'))
->state(fn (Task $task) => $this->getActionOptions()[$task->action] ?? $task->action),
->state(fn (Task $task) => $task->getSchema()?->getName() ?? $task->action),
TextColumn::make('payload')
->label(trans('server/schedule.tasks.payload'))
->state(fn (Task $task) => match ($task->payload) {
'start', 'restart', 'stop', 'kill' => mb_ucfirst($task->payload),
default => explode(PHP_EOL, $task->payload)
})
->state(fn (Task $task) => $task->getSchema()?->formatPayload($task->payload) ?? $task->payload)
->tooltip(fn (Task $task) => $task->getSchema()?->getPayloadLabel())
->placeholder(trans('server/schedule.tasks.no_payload'))
->badge(),
TextColumn::make('time_offset')
->label(trans('server/schedule.tasks.time_offset'))
->hidden(fn () => config('queue.default') === 'sync')
->suffix(fn (Task $task) => $task->sequence_id > 1 ? ' '. trans_choice('server/schedule.tasks.seconds', $task->time_offset) : null)
->state(fn (Task $task) => $task->sequence_id === 1 ? null : $task->time_offset)
->state(fn (Task $task) => $task->isFirst() ? null : $task->time_offset)
->suffix(fn ($state) => ' ' . trans_choice('server/schedule.tasks.seconds', $state))
->placeholder(trans('server/schedule.tasks.first_task')),
IconColumn::make('continue_on_failure')
->label(trans('server/schedule.tasks.continue_on_failure'))

View File

@@ -13,7 +13,6 @@ use App\Filament\Server\Resources\Schedules\Pages\ListSchedules;
use App\Filament\Server\Resources\Schedules\Pages\ViewSchedule;
use App\Filament\Server\Resources\Schedules\RelationManagers\TasksRelationManager;
use App\Helpers\Utilities;
use App\Models\Permission;
use App\Models\Schedule;
use App\Traits\Filament\BlockAccessInConflict;
use App\Traits\Filament\CanCustomizePages;
@@ -26,7 +25,6 @@ use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
@@ -46,7 +44,6 @@ use Filament\Support\Exceptions\Halt;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString;
use Throwable;
@@ -64,26 +61,6 @@ class ScheduleResource extends Resource
protected static string|\BackedEnum|null $navigationIcon = 'tabler-clock';
public static function canViewAny(): bool
{
return user()?->can(Permission::ACTION_SCHEDULE_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return user()?->can(Permission::ACTION_SCHEDULE_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return user()?->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return user()?->can(Permission::ACTION_SCHEDULE_DELETE, Filament::getTenant());
}
/**
* @throws Exception
*/
@@ -255,8 +232,7 @@ class ScheduleResource extends Resource
'6' => trans('server/schedule.time.saturday'),
'0' => trans('server/schedule.time.sunday'),
])
->selectablePlaceholder(false)
->native(false),
->selectablePlaceholder(false),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');
@@ -358,7 +334,8 @@ class ScheduleResource extends Resource
->state(fn (Schedule $schedule) => $schedule->status === ScheduleStatus::Active ? $schedule->next_run_at : null),
])
->recordActions([
ViewAction::make(),
ViewAction::make()
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
DeleteAction::make()
->after(function (Schedule $schedule) {

View File

@@ -37,7 +37,6 @@ use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class UserResource extends Resource
{
@@ -63,26 +62,6 @@ class UserResource extends Resource
return $server->subusers->count();
}
public static function canViewAny(): bool
{
return user()?->can(Permission::ACTION_USER_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return user()?->can(Permission::ACTION_USER_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return user()?->can(Permission::ACTION_USER_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return user()?->can(Permission::ACTION_USER_DELETE, Filament::getTenant());
}
public static function defaultTable(Table $table): Table
{
/** @var Server $server */
@@ -140,6 +119,7 @@ class UserResource extends Resource
DeleteAction::make()
->label(trans('server/user.delete'))
->hidden(fn (User $user) => user()?->id === $user->id)
->successNotificationTitle(null)
->action(function (User $user, SubuserDeletionService $subuserDeletionService) use ($server) {
$subuser = $server->subusers->where('user_id', $user->id)->first();
$subuserDeletionService->handle($subuser, $server);
@@ -154,6 +134,7 @@ class UserResource extends Resource
->hidden(fn (User $user) => user()?->id === $user->id)
->authorize(fn () => user()?->can(Permission::ACTION_USER_UPDATE, $server))
->modalHeading(fn (User $user) => trans('server/user.editing', ['user' => $user->email]))
->successNotificationTitle(null)
->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) {
$subuser = $server->subusers->where('user_id', $user->id)->first();

View File

@@ -77,7 +77,7 @@ class DatabaseHostController extends ApplicationApiController
return $this->fractal->item($databaseHost)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->addMeta([
'resource' => route('api.application.databases.view', [
'resource' => route('api.application.databasehosts.view', [
'database_host' => $databaseHost->id,
]),
])

View File

@@ -34,7 +34,7 @@ class ServerManagementController extends ApplicationApiController
}
/**
* Suspsend
* Suspend
*
* Suspend a server on the Panel.
*

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\Client;
use App\Facades\Activity;
use App\Http\Requests\Api\Client\Account\UpdateEmailRequest;
use App\Http\Requests\Api\Client\Account\UpdatePasswordRequest;
use App\Http\Requests\Api\Client\Account\UpdateUsernameRequest;
use App\Services\Users\UserUpdateService;
use App\Transformers\Api\Client\UserTransformer;
use Illuminate\Auth\AuthManager;
@@ -36,6 +37,25 @@ class AccountController extends ClientApiController
->toArray();
}
/**
* Update username
*
* Update the authenticated user's username.
*/
public function updateUsername(UpdateUsernameRequest $request): JsonResponse
{
$original = $request->user()->username;
$this->updateService->handle($request->user(), $request->validated());
if ($original !== $request->input('username')) {
Activity::event('user:account.username-changed')
->property(['old' => $original, 'new' => $request->input('username')])
->log();
}
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* Update email
*

View File

@@ -55,23 +55,19 @@ class SSHKeyController extends ClientApiController
*
* Deletes an SSH key from the user's account.
*/
public function delete(ClientApiRequest $request): JsonResponse
public function delete(ClientApiRequest $request, string $fingerprint): JsonResponse
{
$request->validate(['fingerprint' => ['required', 'string']]);
/** @var ?UserSSHKey $key */
/** @var UserSSHKey $key */
$key = $request->user()->sshKeys()
->where('fingerprint', $request->input('fingerprint'))
->first();
->where('fingerprint', $fingerprint)
->firstOrFail();
if (!is_null($key)) {
$key->delete();
Activity::event('user:ssh-key.delete')
->subject($key)
->property('fingerprint', $key->fingerprint)
->log();
Activity::event('user:ssh-key.delete')
->subject($key)
->property('fingerprint', $key->fingerprint)
->log();
}
$key->delete();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}

View File

@@ -212,7 +212,8 @@ class FileController extends ClientApiController
$file = $this->fileRepository->setServer($server)->compressFiles(
$request->input('root'),
$request->input('files'),
$request->input('name')
$request->input('name'),
$request->input('extension')
);
Activity::event('server:file.compress')

View File

@@ -170,7 +170,7 @@ class ScheduleTaskController extends ClientApiController
throw new NotFoundHttpException();
}
if (!$request->user()->can(Permission::ACTION_SCHEDULE_UPDATE, $server)) {
if (!$request->user()->can(Permission::ACTION_SCHEDULE_DELETE, $server)) {
throw new HttpForbiddenException('You do not have permission to perform this action.');
}

Some files were not shown because too many files have changed in this diff Show More