mirror of
https://github.com/pelican-dev/panel.git
synced 2026-02-23 19:08:57 +03:00
Compare commits
101 Commits
chore/shif
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfd6dbfe26 | ||
|
|
b4f331e4b2 | ||
|
|
7a95712ed0 | ||
|
|
b6aeb954c4 | ||
|
|
7c0d53c796 | ||
|
|
71bd267166 | ||
|
|
25d8adbcc6 | ||
|
|
27b896c6d2 | ||
|
|
bda2f9a699 | ||
|
|
04375439d7 | ||
|
|
0fe8917668 | ||
|
|
c312ef493f | ||
|
|
6c02f9a663 | ||
|
|
2dd6e3d4fc | ||
|
|
575e5bdb0d | ||
|
|
efa8eef57c | ||
|
|
d16e7dd876 | ||
|
|
897b95ec13 | ||
|
|
97f5a0f20b | ||
|
|
d0af45a0c7 | ||
|
|
78ab098d02 | ||
|
|
cdccca8fa2 | ||
|
|
bb33bcca4f | ||
|
|
611b8649e0 | ||
|
|
b1b723485f | ||
|
|
25c8ff3f1f | ||
|
|
07763d912b | ||
|
|
65bb99e2b0 | ||
|
|
a195b56f93 | ||
|
|
d78c977d75 | ||
|
|
5e25ea4a43 | ||
|
|
886836c60a | ||
|
|
f575e3edfa | ||
|
|
1a66b3fab4 | ||
|
|
0f1efcfd15 | ||
|
|
3f89c6ddd8 | ||
|
|
20cb7850ef | ||
|
|
108dad09fb | ||
|
|
445c9364bc | ||
|
|
acec117b1e | ||
|
|
89199dfbe5 | ||
|
|
216a3484f1 | ||
|
|
5c3b0919aa | ||
|
|
f4ee33fa4f | ||
|
|
d8368c4cec | ||
|
|
aa35d7d001 | ||
|
|
3c25b43b46 | ||
|
|
0891db5342 | ||
|
|
172436e012 | ||
|
|
2b5403a4da | ||
|
|
a30c45fbbe | ||
|
|
b06df23823 | ||
|
|
1ff965611e | ||
|
|
cec141889a | ||
|
|
6ed84b5584 | ||
|
|
49f24e37b6 | ||
|
|
e0c4e47a6c | ||
|
|
4bda7cba75 | ||
|
|
852f7beb39 | ||
|
|
d61583cd7b | ||
|
|
21f9f259d0 | ||
|
|
b2aff5445b | ||
|
|
1f26750a2a | ||
|
|
6d83c6d908 | ||
|
|
574a391e73 | ||
|
|
605fcbe61a | ||
|
|
0214b127e4 | ||
|
|
e6aa76ef2c | ||
|
|
d38075e3cb | ||
|
|
0fec6adc3e | ||
|
|
5e3c22ea5e | ||
|
|
d1a808a746 | ||
|
|
3bcdeea800 | ||
|
|
e6bd6e416f | ||
|
|
8e006ac32d | ||
|
|
430f28a847 | ||
|
|
1a4fa5e67a | ||
|
|
a65469b33b | ||
|
|
d587cf3ee5 | ||
|
|
2cd9fa2cde | ||
|
|
d735e858a2 | ||
|
|
317fa46894 | ||
|
|
e589f972fb | ||
|
|
266e3779d5 | ||
|
|
4652680a7b | ||
|
|
e99f7179c6 | ||
|
|
1f56b8e114 | ||
|
|
574e03a986 | ||
|
|
05f3422dda | ||
|
|
dbe4bdd62d | ||
|
|
f6710dbbe4 | ||
|
|
e4f807b297 | ||
|
|
cd965678b7 | ||
|
|
a58ae874f3 | ||
|
|
432fb8a514 | ||
|
|
bb02ec4c6c | ||
|
|
69b669e345 | ||
|
|
80993f38a9 | ||
|
|
19103b16b8 | ||
|
|
246997754e | ||
|
|
df75dbe2ad |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,6 +21,7 @@ yarn-error.log
|
||||
/.idea
|
||||
/.nova
|
||||
/.vscode
|
||||
/.ddev
|
||||
|
||||
public/assets/manifest.json
|
||||
/database/*.sqlite*
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class DeleteUserCommand extends Command
|
||||
if ($this->input->isInteractive()) {
|
||||
$tableValues = [];
|
||||
foreach ($results as $user) {
|
||||
$tableValues[] = [$user->id, $user->email, $user->name];
|
||||
$tableValues[] = [$user->id, $user->email, $user->username];
|
||||
}
|
||||
|
||||
$this->table(['User ID', 'Email', 'Name'], $tableValues);
|
||||
|
||||
9
app/Enums/CustomRenderHooks.php
Normal file
9
app/Enums/CustomRenderHooks.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum CustomRenderHooks: string
|
||||
{
|
||||
case FooterStart = 'pelican::footer.start';
|
||||
case FooterEnd = 'pelican::footer.end';
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
18
app/Exceptions/Http/TwoFactorAuthRequiredException.php
Normal file
18
app/Exceptions/Http/TwoFactorAuthRequiredException.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
|
||||
->modalHeading('Invalid GSL token')
|
||||
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
|
||||
->modalSubmitActionLabel('Update GSL Token')
|
||||
->disabledSchema(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
|
||||
->disabledSchema(fn () => !user()?->can(Permission::ACTION_STARTUP_UPDATE, $server))
|
||||
->schema([
|
||||
TextEntry::make('info')
|
||||
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
|
||||
|
||||
@@ -44,7 +44,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
|
||||
->modalHeading('Unsupported Java Version')
|
||||
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
|
||||
->modalSubmitActionLabel('Update Docker Image')
|
||||
->disabledSchema(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
|
||||
->disabledSchema(fn () => !user()?->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
|
||||
->schema([
|
||||
TextEntry::make('java')
|
||||
->label('Please select a supported version from the list below to continue starting the server.'),
|
||||
@@ -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 {
|
||||
|
||||
@@ -32,9 +32,9 @@ class PIDLimitSchema implements FeatureSchemaInterface
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->icon('tabler-alert-triangle')
|
||||
->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
|
||||
->modalHeading(fn () => user()?->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
|
||||
->modalDescription(new HtmlString(Blade::render(
|
||||
auth()->user()->isAdmin() ? <<<'HTML'
|
||||
user()?->isAdmin() ? <<<'HTML'
|
||||
<p>
|
||||
This server has reached the maximum process or memory limit.
|
||||
</p>
|
||||
|
||||
@@ -29,7 +29,7 @@ class SteamDiskSpaceSchema implements FeatureSchemaInterface
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Out of available disk space...')
|
||||
->modalDescription(new HtmlString(Blade::render(
|
||||
auth()->user()->isAdmin() ? <<<'HTML'
|
||||
user()?->isAdmin() ? <<<'HTML'
|
||||
<p>
|
||||
This server has run out of available disk space and cannot complete the install or update
|
||||
process.
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Extensions\OAuth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Laravel\Socialite\Contracts\User as OAuthUser;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
class OAuthService
|
||||
@@ -43,4 +45,27 @@ class OAuthService
|
||||
|
||||
$this->schemas[$schema->getId()] = $schema;
|
||||
}
|
||||
|
||||
public function linkUser(User $user, OAuthSchemaInterface $schema, OAuthUser $oauthUser): User
|
||||
{
|
||||
$oauth = $user->oauth ?? [];
|
||||
$oauth[$schema->getId()] = $oauthUser->getId();
|
||||
|
||||
$user->update(['oauth' => $oauth]);
|
||||
|
||||
return $user->refresh();
|
||||
}
|
||||
|
||||
public function unlinkUser(User $user, OAuthSchemaInterface $schema): User
|
||||
{
|
||||
$oauth = $user->oauth ?? [];
|
||||
if (!isset($oauth[$schema->getId()])) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
unset($oauth[$schema->getId()]);
|
||||
$user->update(['oauth' => $oauth]);
|
||||
|
||||
return $user->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
45
app/Extensions/OAuth/Schemas/BitbucketSchema.php
Normal file
45
app/Extensions/OAuth/Schemas/BitbucketSchema.php
Normal 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';
|
||||
}
|
||||
}
|
||||
48
app/Extensions/OAuth/Schemas/FacebookSchema.php
Normal file
48
app/Extensions/OAuth/Schemas/FacebookSchema.php
Normal 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';
|
||||
}
|
||||
}
|
||||
54
app/Extensions/OAuth/Schemas/GoogleSchema.php
Normal file
54
app/Extensions/OAuth/Schemas/GoogleSchema.php
Normal 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';
|
||||
}
|
||||
}
|
||||
45
app/Extensions/OAuth/Schemas/LinkedinSchema.php
Normal file
45
app/Extensions/OAuth/Schemas/LinkedinSchema.php
Normal 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';
|
||||
}
|
||||
}
|
||||
45
app/Extensions/OAuth/Schemas/SlackSchema.php
Normal file
45
app/Extensions/OAuth/Schemas/SlackSchema.php
Normal 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';
|
||||
}
|
||||
}
|
||||
54
app/Extensions/OAuth/Schemas/XSchema.php
Normal file
54
app/Extensions/OAuth/Schemas/XSchema.php
Normal 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';
|
||||
}
|
||||
}
|
||||
32
app/Extensions/Tasks/Schemas/CreateBackupSchema.php
Normal file
32
app/Extensions/Tasks/Schemas/CreateBackupSchema.php
Normal 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');
|
||||
}
|
||||
}
|
||||
26
app/Extensions/Tasks/Schemas/DeleteFilesSchema.php
Normal file
26
app/Extensions/Tasks/Schemas/DeleteFilesSchema.php
Normal 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');
|
||||
}
|
||||
}
|
||||
57
app/Extensions/Tasks/Schemas/PowerActionSchema.php
Normal file
57
app/Extensions/Tasks/Schemas/PowerActionSchema.php
Normal 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()),
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Extensions/Tasks/Schemas/SendCommandSchema.php
Normal file
36
app/Extensions/Tasks/Schemas/SendCommandSchema.php
Normal 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()),
|
||||
];
|
||||
}
|
||||
}
|
||||
52
app/Extensions/Tasks/Schemas/TaskSchema.php
Normal file
52
app/Extensions/Tasks/Schemas/TaskSchema.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Extensions/Tasks/TaskSchemaInterface.php
Normal file
28
app/Extensions/Tasks/TaskSchemaInterface.php
Normal 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;
|
||||
}
|
||||
37
app/Extensions/Tasks/TaskService.php
Normal file
37
app/Extensions/Tasks/TaskService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -39,7 +40,7 @@ class Health extends Page
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view health');
|
||||
return user()?->can('view health');
|
||||
}
|
||||
|
||||
protected function getActions(): array
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
129
app/Filament/Admin/Pages/ListLogs.php
Normal file
129
app/Filament/Admin/Pages/ListLogs.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -82,7 +83,7 @@ class Settings extends Page implements HasSchemas
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view settings');
|
||||
return user()?->can('view settings');
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
@@ -106,7 +107,7 @@ class Settings extends Page implements HasSchemas
|
||||
Tabs::make('Tabs')
|
||||
->columns()
|
||||
->persistTabInQueryString()
|
||||
->disabled(fn () => !auth()->user()->can('update settings'))
|
||||
->disabled(fn () => !user()?->can('update settings'))
|
||||
->tabs([
|
||||
Tab::make('general')
|
||||
->label(trans('admin/setting.navigation.general'))
|
||||
@@ -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'))),
|
||||
@@ -233,12 +241,12 @@ class Settings extends Page implements HasSchemas
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->authorize(fn () => user()?->can('update settings'))
|
||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
||||
Action::make('cloudflare')
|
||||
->label(trans('admin/setting.general.set_to_cf'))
|
||||
->icon('tabler-brand-cloudflare')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->authorize(fn () => user()?->can('update settings'))
|
||||
->action(function (Factory $client, Set $set) {
|
||||
$ips = collect();
|
||||
|
||||
@@ -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)),
|
||||
@@ -335,7 +345,7 @@ class Settings extends Page implements HasSchemas
|
||||
->label(trans('admin/setting.mail.test_mail'))
|
||||
->icon('tabler-send')
|
||||
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->authorize(fn () => user()?->can('update settings'))
|
||||
->action(function (Get $get) {
|
||||
// Store original mail configuration
|
||||
$originalConfig = [
|
||||
@@ -368,8 +378,8 @@ class Settings extends Page implements HasSchemas
|
||||
'services.mailgun.endpoint' => $get('MAILGUN_ENDPOINT'),
|
||||
]);
|
||||
|
||||
MailNotification::route('mail', auth()->user()->email)
|
||||
->notify(new MailTested(auth()->user()));
|
||||
MailNotification::route('mail', user()?->email)
|
||||
->notify(new MailTested(user()));
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/setting.mail.test_mail_sent'))
|
||||
@@ -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,8 +846,10 @@ class Settings extends Page implements HasSchemas
|
||||
{
|
||||
return [
|
||||
Action::make('save')
|
||||
->iconButton()->iconSize(IconSize::ExtraLarge)
|
||||
->icon('tabler-device-floppy')
|
||||
->action('save')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->authorize(fn () => user()?->can('update settings'))
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
|
||||
|
||||
104
app/Filament/Admin/Pages/ViewLogs.php
Normal file
104
app/Filament/Admin/Pages/ViewLogs.php
Normal 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;
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -93,17 +93,15 @@ class ApiKeyResource extends Resource
|
||||
->sortable(),
|
||||
TextColumn::make('user.username')
|
||||
->label(trans('admin/apikey.table.created_by'))
|
||||
->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
|
||||
->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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -38,7 +41,7 @@ class CreateApiKey extends CreateRecord
|
||||
{
|
||||
$data['identifier'] = ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION);
|
||||
$data['token'] = Str::random(ApiKey::KEY_LENGTH);
|
||||
$data['user_id'] = auth()->user()->id;
|
||||
$data['user_id'] = user()?->id;
|
||||
$data['key_type'] = ApiKey::TYPE_APPLICATION;
|
||||
|
||||
$permissions = [];
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +162,7 @@ class DatabaseHostResource extends Resource
|
||||
->preload()
|
||||
->helperText(trans('admin/databasehost.linked_nodes_help'))
|
||||
->label(trans('admin/databasehost.linked_nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'))),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -196,7 +192,7 @@ class DatabaseHostResource extends Resource
|
||||
|
||||
return $query->where(function (Builder $query) {
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
$query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class CreateDatabaseHost extends CreateRecord
|
||||
->preload()
|
||||
->helperText(trans('admin/databasehost.linked_nodes_help'))
|
||||
->label(trans('admin/databasehost.linked_nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class EggResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return auth()->user()->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
|
||||
return user()?->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -83,14 +86,16 @@ class CreateEgg extends CreateRecord
|
||||
->rows(2)
|
||||
->columnSpanFull()
|
||||
->helperText(trans('admin/egg.description_help')),
|
||||
Textarea::make('startup')
|
||||
->label(trans('admin/egg.startup'))
|
||||
->rows(3)
|
||||
KeyValue::make('startup_commands')
|
||||
->label(trans('admin/egg.startup_commands'))
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->placeholder(implode("\n", [
|
||||
'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}',
|
||||
]))
|
||||
->addActionLabel(trans('admin/egg.add_startup'))
|
||||
->keyLabel(trans('admin/egg.startup_name'))
|
||||
->keyPlaceholder('Default')
|
||||
->valueLabel(trans('admin/egg.startup_command'))
|
||||
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
|
||||
->helperText(trans('admin/egg.startup_help')),
|
||||
TagsInput::make('file_denylist')
|
||||
->label(trans('admin/egg.file_denylist'))
|
||||
@@ -122,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')),
|
||||
]),
|
||||
|
||||
@@ -255,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([
|
||||
|
||||
@@ -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,64 +57,242 @@ 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')),
|
||||
Textarea::make('startup')
|
||||
->label(trans('admin/egg.startup'))
|
||||
->rows(3)
|
||||
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()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->addActionLabel(trans('admin/egg.add_startup'))
|
||||
->keyLabel(trans('admin/egg.startup_name'))
|
||||
->valueLabel(trans('admin/egg.startup_command'))
|
||||
->helperText(trans('admin/egg.startup_help')),
|
||||
TagsInput::make('file_denylist')
|
||||
->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()
|
||||
@@ -245,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',
|
||||
@@ -267,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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,20 +64,22 @@ 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) {
|
||||
$replica->author = auth()->user()->email;
|
||||
$replica->author = user()?->email;
|
||||
$replica->name .= ' Copy';
|
||||
$replica->uuid = Str::uuid()->toString();
|
||||
})
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,11 +159,12 @@ 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'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
|
||||
->searchable(['name', 'fqdn'])
|
||||
->preload(),
|
||||
]),
|
||||
@@ -197,7 +195,7 @@ class MountResource extends Resource
|
||||
|
||||
return $query->where(function (Builder $query) {
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
$query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -44,7 +44,7 @@ class NodeResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return auth()->user()->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
|
||||
return user()?->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -57,7 +57,7 @@ class NodeResource extends Resource
|
||||
{
|
||||
return [
|
||||
AllocationsRelationManager::class,
|
||||
NodesRelationManager::class,
|
||||
ServersRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -75,6 +75,6 @@ class NodeResource extends Resource
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
return $query->whereIn('id', user()?->accessibleNodes()->pluck('id'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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,16 +14,20 @@ 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;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Infolists\Components\CodeEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
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;
|
||||
@@ -32,8 +36,12 @@ 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;
|
||||
|
||||
class EditNode extends EditRecord
|
||||
@@ -43,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;
|
||||
}
|
||||
|
||||
@@ -547,11 +555,12 @@ class EditNode extends EditRecord
|
||||
->label(trans('admin/node.instructions'))
|
||||
->columnSpanFull()
|
||||
->state(new HtmlString(trans('admin/node.instructions_help'))),
|
||||
Textarea::make('config')
|
||||
CodeEntry::make('config')
|
||||
->label('/etc/pelican/config.yml')
|
||||
->grammar(Grammar::Yaml)
|
||||
->state(fn (Node $node) => $node->getYamlConfiguration())
|
||||
->copyable()
|
||||
->disabled()
|
||||
->rows(19)
|
||||
->hintCopy()
|
||||
->columnSpanFull(),
|
||||
Grid::make()
|
||||
->columns()
|
||||
@@ -621,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)),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -629,8 +786,6 @@ class EditNode extends EditRecord
|
||||
{
|
||||
$node = Node::findOrFail($data['id']);
|
||||
|
||||
$data['config'] = $node->getYamlConfiguration();
|
||||
|
||||
if (!is_ip($node->fqdn)) {
|
||||
$ip = get_ip_from_hostname($node->fqdn);
|
||||
if ($ip) {
|
||||
@@ -655,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'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -680,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) {
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +124,9 @@ class AllocationsRelationManager extends RelationManager
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())),
|
||||
UpdateNodeAllocations::make()
|
||||
->nodeRecord($this->getOwnerRecord())
|
||||
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
@@ -32,7 +32,7 @@ class NodeCpuChart extends ChartWidget
|
||||
$this->cpuHistory = session("{$sessionKey}.cpu_history", []);
|
||||
$this->cpuHistory[] = [
|
||||
'cpu' => round($data['cpu_percent'] * $this->threads, 2),
|
||||
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
'timestamp' => now(user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
];
|
||||
|
||||
$this->cpuHistory = array_slice($this->cpuHistory, -60);
|
||||
@@ -50,7 +50,7 @@ class NodeCpuChart extends ChartWidget
|
||||
],
|
||||
],
|
||||
'labels' => array_column($this->cpuHistory, 'timestamp'),
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
'locale' => user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class NodeMemoryChart extends ChartWidget
|
||||
'memory' => round(config('panel.use_binary_prefix')
|
||||
? $data['memory_used'] / 1024 / 1024 / 1024
|
||||
: $data['memory_used'] / 1000 / 1000 / 1000, 2),
|
||||
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
'timestamp' => now(user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
];
|
||||
|
||||
$this->memoryHistory = array_slice($this->memoryHistory, -60);
|
||||
@@ -52,7 +52,7 @@ class NodeMemoryChart extends ChartWidget
|
||||
],
|
||||
],
|
||||
'labels' => array_column($this->memoryHistory, 'timestamp'),
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
'locale' => user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class NodeStorageChart extends ChartWidget
|
||||
],
|
||||
],
|
||||
'labels' => [trans('admin/node.used'), trans('admin/node.unused')],
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
'locale' => user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -65,7 +64,7 @@ class RoleResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return auth()->user()->getCustomization(CustomizationKey::TopNavigation) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
|
||||
return user()?->getCustomization(CustomizationKey::TopNavigation) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -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')))
|
||||
|
||||
@@ -116,8 +116,16 @@ 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 = auth()->user()->accessibleNodes()->latest()->first();
|
||||
$latestNode = user()?->accessibleNodes()->latest()->first();
|
||||
$this->node = $latestNode;
|
||||
|
||||
return $this->node?->id;
|
||||
@@ -128,7 +136,7 @@ class CreateServer extends CreateRecord
|
||||
'md' => 2,
|
||||
])
|
||||
->live()
|
||||
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', user()?->accessibleNodes()->pluck('id')))
|
||||
->searchable()
|
||||
->required()
|
||||
->preload()
|
||||
@@ -141,7 +149,7 @@ class CreateServer extends CreateRecord
|
||||
->preload()
|
||||
->prefixIcon('tabler-user')
|
||||
->selectablePlaceholder(false)
|
||||
->default(auth()->user()->id)
|
||||
->default(user()?->id)
|
||||
->label(trans('admin/server.owner'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
@@ -151,7 +159,7 @@ class CreateServer extends CreateRecord
|
||||
->relationship('user', 'username')
|
||||
->searchable(['username', 'email'])
|
||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->username ($user->email)")
|
||||
->createOptionAction(fn (Action $action) => $action->authorize(fn () => auth()->user()->can('create', User::class)))
|
||||
->createOptionAction(fn (Action $action) => $action->authorize(fn () => user()?->can('create', User::class)))
|
||||
->createOptionForm([
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/user.username'))
|
||||
@@ -212,7 +220,7 @@ class CreateServer extends CreateRecord
|
||||
->where('node_id', $get('node_id'))
|
||||
->whereNull('server_id'),
|
||||
)
|
||||
->createOptionAction(fn (Action $action) => $action->authorize(fn (Get $get) => auth()->user()->can('create', Node::find($get('node_id')))))
|
||||
->createOptionAction(fn (Action $action) => $action->authorize(fn (Get $get) => user()?->can('create', Node::find($get('node_id')))))
|
||||
->createOptionForm(function (Get $get) {
|
||||
$getPage = $get;
|
||||
|
||||
@@ -328,7 +336,7 @@ class CreateServer extends CreateRecord
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get, $old) {
|
||||
$egg = Egg::query()->find($state);
|
||||
$set('startup', $egg->startup ?? '');
|
||||
$set('startup', '');
|
||||
$set('image', '');
|
||||
|
||||
$variables = $egg->variables ?? [];
|
||||
@@ -402,24 +410,46 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->inline(),
|
||||
|
||||
Textarea::make('startup')
|
||||
->hintIcon('tabler-code')
|
||||
Select::make('select_startup')
|
||||
->label(trans('admin/server.startup_cmd'))
|
||||
->hidden(fn (Get $get) => $get('egg_id') === null)
|
||||
->required()
|
||||
->live()
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
1
|
||||
);
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('startup', $state))
|
||||
->options(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$startups = $egg->startup_commands ?? [];
|
||||
|
||||
$currentStartup = $get('startup');
|
||||
if (!$currentStartup && $startups) {
|
||||
$currentStartup = collect($startups)->first();
|
||||
$set('startup', $currentStartup);
|
||||
$set('select_startup', $currentStartup);
|
||||
}
|
||||
|
||||
return array_flip($startups) + ['custom' => 'Custom Startup'];
|
||||
})
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
]),
|
||||
->selectablePlaceholder(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
Textarea::make('startup')
|
||||
->hiddenLabel()
|
||||
->hidden(fn (Get $get) => $get('egg_id') === null)
|
||||
->required()
|
||||
->live()
|
||||
->autosize()
|
||||
->afterStateUpdated(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$startups = $egg->startup_commands ?? [];
|
||||
|
||||
if (in_array($state, $startups)) {
|
||||
$set('select_startup', $state);
|
||||
} else {
|
||||
$set('select_startup', 'custom');
|
||||
}
|
||||
})
|
||||
->placeholder(trans('admin/server.startup_placeholder'))
|
||||
->columnSpanFull(),
|
||||
|
||||
Hidden::make('environment')->default([]),
|
||||
|
||||
@@ -497,12 +527,12 @@ class CreateServer extends CreateRecord
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label(trans('admin/server.cpu_limit'))->inlineLabel()
|
||||
->suffix('%')
|
||||
->hintIcon('tabler-question-mark', trans('admin/server.cpu_helper'))
|
||||
->default(0)
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->helperText(trans('admin/server.cpu_helper')),
|
||||
->minValue(0),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
@@ -808,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) {
|
||||
|
||||
@@ -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()
|
||||
@@ -210,7 +390,7 @@ class EditServer extends EditRecord
|
||||
->maxLength(255),
|
||||
Select::make('node_id')
|
||||
->label(trans('admin/server.node'))
|
||||
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', user()?->accessibleNodes()->pluck('id')))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 1,
|
||||
@@ -258,6 +438,7 @@ class EditServer extends EditRecord
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label(trans('admin/server.cpu_limit'))->inlineLabel()
|
||||
->suffix('%')
|
||||
->hintIcon('tabler-question-mark', trans('admin/server.cpu_helper'))
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
@@ -609,26 +790,67 @@ class EditServer extends EditRecord
|
||||
1 => 'tabler-code-off',
|
||||
])
|
||||
->required(),
|
||||
|
||||
Hidden::make('previewing')
|
||||
->default(false),
|
||||
Textarea::make('startup')
|
||||
|
||||
Select::make('select_startup')
|
||||
->label(trans('admin/server.startup_cmd'))
|
||||
->required()
|
||||
->columnSpan(6)
|
||||
->autosize()
|
||||
->live()
|
||||
->afterStateUpdated(function (Set $set, $state) {
|
||||
$set('startup', $state);
|
||||
$set('previewing', false);
|
||||
})
|
||||
->options(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::find($get('egg_id'));
|
||||
$startups = $egg->startup_commands ?? [];
|
||||
|
||||
$currentStartup = $get('startup');
|
||||
if (!$currentStartup && $startups) {
|
||||
$currentStartup = collect($startups)->first();
|
||||
$set('startup', $currentStartup);
|
||||
$set('select_startup', $currentStartup);
|
||||
}
|
||||
|
||||
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()
|
||||
->hintAction(PreviewStartupAction::make('preview')),
|
||||
|
||||
Textarea::make('defaultStartup')
|
||||
->hintCopy()
|
||||
->label(trans('admin/server.default_startup'))
|
||||
->disabled()
|
||||
Textarea::make('startup')
|
||||
->hiddenLabel()
|
||||
->required()
|
||||
->live()
|
||||
->autosize()
|
||||
->columnSpan(6)
|
||||
->formatStateUsing(function ($state, Get $get) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
->afterStateUpdated(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::find($get('egg_id'));
|
||||
$startups = $egg->startup_commands ?? [];
|
||||
|
||||
return $egg->startup;
|
||||
}),
|
||||
if (in_array($state, $startups)) {
|
||||
$set('select_startup', $state);
|
||||
} else {
|
||||
$set('select_startup', 'custom');
|
||||
}
|
||||
})
|
||||
->placeholder(trans('admin/server.startup_placeholder'))
|
||||
->columnSpanFull(),
|
||||
|
||||
Repeater::make('server_variables')
|
||||
->hiddenLabel()
|
||||
@@ -785,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) {
|
||||
@@ -857,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)
|
||||
@@ -916,7 +1138,9 @@ class EditServer extends EditRecord
|
||||
}
|
||||
})
|
||||
->hidden(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => auth()->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'))
|
||||
@@ -933,12 +1157,15 @@ class EditServer extends EditRecord
|
||||
}
|
||||
})
|
||||
->visible(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
->authorize(fn (Server $server) => user()?->can('delete server', $server)),
|
||||
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'),
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
@@ -69,7 +73,7 @@ class ListServers extends ListRecords
|
||||
->searchable(),
|
||||
SelectColumn::make('allocation_id')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->hidden(fn () => !auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->hidden(fn () => !user()?->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->disabled(fn (Server $server) => $server->allocations->count() <= 1)
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
|
||||
->selectablePlaceholder(fn (Server $server) => $server->allocations->count() <= 1)
|
||||
@@ -77,7 +81,7 @@ class ListServers extends ListRecords
|
||||
->sortable(),
|
||||
TextColumn::make('allocation_id_readonly')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->hidden(fn () => auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->hidden(fn () => user()?->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->state(fn (Server $server) => $server->allocation->address ?? trans('admin/server.none')),
|
||||
TextColumn::make('image')->hidden(),
|
||||
TextColumn::make('backups_count')
|
||||
@@ -89,17 +93,17 @@ 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) => auth()->user()->canAccessTenant($server)),
|
||||
->authorize(fn (Server $server) => user()?->canAccessTenant($server)),
|
||||
EditAction::make(),
|
||||
])
|
||||
->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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]]);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -47,7 +47,7 @@ class ServerResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return auth()->user()->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
|
||||
return user()?->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -103,6 +103,6 @@ class ServerResource extends Resource
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->whereIn('node_id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
return $query->whereIn('node_id', user()?->accessibleNodes()->pluck('id'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -32,9 +33,12 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->label(fn (User $user) => auth()->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) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
->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)
|
||||
->iconButton()->iconSize(IconSize::ExtraLarge),
|
||||
$this->getSaveFormAction()->formId('form')
|
||||
->iconButton()->iconSize(IconSize::ExtraLarge)
|
||||
->icon('tabler-device-floppy'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -48,8 +52,7 @@ class EditUser extends EditRecord
|
||||
if (!$record instanceof User) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
unset($data['roles']);
|
||||
unset($data['roles'], $data['avatar']);
|
||||
|
||||
return $this->service->handle($record, $data);
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class ServersRelationManager extends RelationManager
|
||||
->sortable(),
|
||||
SelectColumn::make('allocation_id')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->hidden(fn () => !auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->hidden(fn () => !user()?->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->disabled(fn (Server $server) => $server->allocations->count() <= 1)
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
|
||||
->selectablePlaceholder(fn (Server $server) => $server->allocations->count() <= 1)
|
||||
@@ -75,7 +75,7 @@ class ServersRelationManager extends RelationManager
|
||||
->sortable(),
|
||||
TextColumn::make('allocation_id_readonly')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->hidden(fn () => auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->hidden(fn () => user()?->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->state(fn (Server $server) => $server->allocation->address ?? trans('admin/server.none')),
|
||||
TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
|
||||
@@ -3,33 +3,56 @@
|
||||
namespace App\Filament\Admin\Resources\Users;
|
||||
|
||||
use App\Enums\CustomizationKey;
|
||||
use App\Extensions\OAuth\OAuthService;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Admin\Resources\Users\Pages\CreateUser;
|
||||
use App\Filament\Admin\Resources\Users\Pages\EditUser;
|
||||
use App\Filament\Admin\Resources\Users\Pages\ListUsers;
|
||||
use App\Filament\Admin\Resources\Users\Pages\ViewUser;
|
||||
use App\Filament\Admin\Resources\Users\RelationManagers\ServersRelationManager;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Models\UserSSHKey;
|
||||
use App\Services\Helpers\LanguageService;
|
||||
use App\Traits\Filament\CanCustomizePages;
|
||||
use App\Traits\Filament\CanCustomizeRelations;
|
||||
use App\Traits\Filament\CanModifyForm;
|
||||
use App\Traits\Filament\CanModifyTable;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Auth\Notifications\ResetPassword;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Actions;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Auth\Events\PasswordResetLinkSent;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class UserResource extends Resource
|
||||
{
|
||||
@@ -61,7 +84,7 @@ class UserResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return auth()->user()->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.user');
|
||||
return user()?->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.user');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -107,10 +130,10 @@ 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) => auth()->user()->id !== $user->id && !$user->servers_count)
|
||||
->checkIfRecordIsSelectableUsing(fn (User $user) => user()?->id !== $user->id && !$user->servers_count)
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make(),
|
||||
]);
|
||||
@@ -119,44 +142,324 @@ class UserResource extends Resource
|
||||
public static function defaultForm(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->columns(['default' => 1, 'lg' => 3])
|
||||
->columns(['default' => 1, 'lg' => 3, 'md' => 2])
|
||||
->components([
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/user.username'))
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
->label(trans('admin/user.email'))
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->label(trans('admin/user.password'))
|
||||
->hintIcon(fn ($operation) => $operation === 'create' ? 'tabler-question-mark' : null, fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
|
||||
->password(),
|
||||
CheckboxList::make('roles')
|
||||
->hidden(fn (?User $user) => $user && $user->isRootAdmin())
|
||||
->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
|
||||
->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
|
||||
->dehydrated()
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
CheckboxList::make('root_admin_role')
|
||||
->visible(fn (?User $user) => $user && $user->isRootAdmin())
|
||||
->disabled()
|
||||
->options([
|
||||
'root_admin' => Role::ROOT_ADMIN,
|
||||
])
|
||||
->descriptions([
|
||||
'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
|
||||
])
|
||||
->formatStateUsing(fn () => ['root_admin'])
|
||||
->dehydrated(false)
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull(),
|
||||
Tabs::make()
|
||||
->schema([
|
||||
Tab::make('account')
|
||||
->label(trans('profile.tabs.account'))
|
||||
->icon('tabler-user-cog')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/user.username'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
->label(trans('admin/user.email'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->label(trans('admin/user.password'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->hintIcon(fn ($operation) => $operation === 'create' ? 'tabler-question-mark' : null, fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
|
||||
->password()
|
||||
->hintAction(
|
||||
Action::make('password_reset')
|
||||
->label(trans('admin/user.password_reset'))
|
||||
->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([
|
||||
'email' => $user->email,
|
||||
],
|
||||
function (User $user, string $token) {
|
||||
$notification = new ResetPassword($token);
|
||||
$notification->url = Filament::getPanel('app')->getResetPasswordUrl($token, $user);
|
||||
|
||||
$user->notify($notification);
|
||||
|
||||
event(new PasswordResetLinkSent($user));
|
||||
},
|
||||
);
|
||||
|
||||
if ($status === Password::RESET_LINK_SENT) {
|
||||
Notification::make()
|
||||
->title(trans('admin/user.password_reset_sent'))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title(trans('admin/user.password_reset_failed'))
|
||||
->body($status)
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})),
|
||||
TextInput::make('external_id')
|
||||
->label(trans('admin/user.external_id'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Select::make('timezone')
|
||||
->label(trans('profile.timezone'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->default(fn () => config('app.timezone', 'UTC'))
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable(),
|
||||
Select::make('language')
|
||||
->label(trans('profile.language'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->prefixIcon('tabler-flag')
|
||||
->live()
|
||||
->default('en')
|
||||
->searchable()
|
||||
->selectablePlaceholder(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()
|
||||
->directory('avatars')
|
||||
->disk('public')
|
||||
->formatStateUsing(function (FileUpload $fileUpload, ?User $user) {
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
$path = $fileUpload->getDirectory() . '/' . $user->id . '.png';
|
||||
if ($fileUpload->getDisk()->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
})
|
||||
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
|
||||
if ($file instanceof TemporaryUploadedFile) {
|
||||
return $file->delete();
|
||||
}
|
||||
|
||||
if ($fileUpload->getDisk()->exists($file)) {
|
||||
return $fileUpload->getDisk()->delete($file);
|
||||
}
|
||||
}),
|
||||
Section::make(trans('profile.tabs.oauth'))
|
||||
->visible(fn (?User $user) => $user)
|
||||
->collapsible()
|
||||
->columnSpanFull()
|
||||
->schema(function (OAuthService $oauthService, ?User $user) {
|
||||
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
$actions = [];
|
||||
foreach ($user->oauth ?? [] as $schema => $_) {
|
||||
$schema = $oauthService->get($schema);
|
||||
if (!$schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $schema->getId();
|
||||
$name = $schema->getName();
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(trans('profile.unlink', ['name' => $name]))
|
||||
->icon('tabler-unlink')
|
||||
->requiresConfirmation()
|
||||
->color(Color::hex($schema->getHexColor()))
|
||||
->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
|
||||
$oauthService->unlinkUser($user, $schema);
|
||||
$livewire->form->fill($user->attributesToArray());
|
||||
Notification::make()
|
||||
->title(trans('profile.unlinked', ['name' => $name]))
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
if (!$actions) {
|
||||
return [
|
||||
TextEntry::make('no_oauth')
|
||||
->state(trans('profile.no_oauth'))
|
||||
->hiddenLabel(),
|
||||
];
|
||||
}
|
||||
|
||||
return [Actions::make($actions)];
|
||||
}),
|
||||
]),
|
||||
Tab::make('roles')
|
||||
->label(trans('admin/user.roles'))
|
||||
->icon('tabler-users-group')
|
||||
->components([
|
||||
CheckboxList::make('roles')
|
||||
->hidden(fn (?User $user) => $user && $user->isRootAdmin())
|
||||
->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
|
||||
->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
|
||||
->dehydrated()
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
CheckboxList::make('root_admin_role')
|
||||
->visible(fn (?User $user) => $user && $user->isRootAdmin())
|
||||
->disabled()
|
||||
->options([
|
||||
'root_admin' => Role::ROOT_ADMIN,
|
||||
])
|
||||
->descriptions([
|
||||
'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
|
||||
])
|
||||
->formatStateUsing(fn () => ['root_admin'])
|
||||
->dehydrated(false)
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Tab::make('keys')
|
||||
->visible(fn (?User $user) => $user)
|
||||
->label(trans('profile.tabs.keys'))
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Section::make(trans('profile.api_keys'))
|
||||
->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('api_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, ?User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']] ?? null;
|
||||
|
||||
if ($key) {
|
||||
$apiKey = ApiKey::find($key['id']);
|
||||
if ($apiKey?->exists()) {
|
||||
$apiKey->delete();
|
||||
|
||||
Activity::event('user:api-key.delete')
|
||||
->actor(user())
|
||||
->subject($user)
|
||||
->subject($apiKey)
|
||||
->property('identifier', $apiKey->identifier)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
$component->state($items);
|
||||
$component->callAfterStateUpdated();
|
||||
}
|
||||
});
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('memo')
|
||||
->hiddenLabel()
|
||||
->state(fn (ApiKey $key) => $key->memo),
|
||||
])
|
||||
->visible(fn (User $user) => $user->apiKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_api_keys')
|
||||
->state(trans('profile.no_api_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->apiKeys()->exists()),
|
||||
]),
|
||||
Section::make(trans('profile.ssh_keys'))->columnSpan(2)
|
||||
->schema([
|
||||
Repeater::make('ssh_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('sshKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['name'])
|
||||
->deleteAction(function (Action $action) {
|
||||
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
|
||||
$items = $component->getState();
|
||||
$key = $items[$arguments['item']];
|
||||
|
||||
$sshKey = UserSSHKey::find($key['id'] ?? null);
|
||||
if ($sshKey->exists()) {
|
||||
$sshKey->delete();
|
||||
|
||||
Activity::event('user:ssh-key.delete')
|
||||
->actor(user())
|
||||
->subject($user)
|
||||
->subject($sshKey)
|
||||
->property('fingerprint', $sshKey->fingerprint)
|
||||
->log();
|
||||
}
|
||||
|
||||
unset($items[$arguments['item']]);
|
||||
|
||||
$component->state($items);
|
||||
|
||||
$component->callAfterStateUpdated();
|
||||
});
|
||||
})
|
||||
->schema(fn () => [
|
||||
TextEntry::make('fingerprint')
|
||||
->hiddenLabel()
|
||||
->state(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
|
||||
])
|
||||
->visible(fn (User $user) => $user->sshKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_ssh_keys')
|
||||
->state(trans('profile.no_ssh_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->sshKeys()->exists()),
|
||||
]),
|
||||
]),
|
||||
Tab::make('activity')
|
||||
->visible(fn (?User $user) => $user)
|
||||
->disabledOn('create')
|
||||
->label(trans('profile.tabs.activity'))
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('log')
|
||||
->hiddenLabel()
|
||||
->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
])->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -43,7 +43,18 @@ class UpdateWidget extends FormWidget
|
||||
->iconColor('warning')
|
||||
->schema([
|
||||
TextEntry::make('info')
|
||||
->hiddenLabel()
|
||||
->state(trans('admin/dashboard.sections.intro-update-available.content', ['latestVersion' => $this->softwareVersionService->latestPanelVersion()])),
|
||||
Section::make(trans('admin/dashboard.sections.intro-update-available.button_changelog'))
|
||||
->icon('tabler-script')
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextEntry::make('changelog')
|
||||
->hiddenLabel()
|
||||
->state($this->softwareVersionService->latestPanelVersionChangelog())
|
||||
->markdown(),
|
||||
]),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('update')
|
||||
|
||||
@@ -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,35 +87,39 @@ 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)),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
$baseQuery = auth()->user()->accessibleServers();
|
||||
$baseQuery = user()?->accessibleServers();
|
||||
|
||||
$usingGrid = auth()->user()->getCustomization(CustomizationKey::DashboardLayout) === 'grid';
|
||||
$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())
|
||||
@@ -139,9 +151,9 @@ class ListServers extends ListRecords
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
$all = auth()->user()->accessibleServers();
|
||||
$my = (clone $all)->where('owner_id', auth()->user()->id);
|
||||
$other = (clone $all)->whereNot('owner_id', auth()->user()->id);
|
||||
$all = user()?->accessibleServers();
|
||||
$my = (clone $all)->where('owner_id', user()?->id);
|
||||
$other = (clone $all)->whereNot('owner_id', user()?->id);
|
||||
|
||||
return [
|
||||
'my' => Tab::make('my')
|
||||
@@ -232,21 +244,21 @@ class ListServers extends ListRecords
|
||||
->label(trans('server/console.power_actions.start'))
|
||||
->color('primary')
|
||||
->icon('tabler-player-play-filled')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->authorize(fn (Server $server) => user()?->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->visible(fn (Server $server) => $server->retrieveStatus()->isStartable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']),
|
||||
Action::make('restart')
|
||||
->label(trans('server/console.power_actions.restart'))
|
||||
->color('gray')
|
||||
->icon('tabler-reload')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->authorize(fn (Server $server) => user()?->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->visible(fn (Server $server) => $server->retrieveStatus()->isRestartable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']),
|
||||
Action::make('stop')
|
||||
->label(trans('server/console.power_actions.stop'))
|
||||
->color('danger')
|
||||
->icon('tabler-player-stop-filled')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->authorize(fn (Server $server) => user()?->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn (Server $server) => $server->retrieveStatus()->isStoppable() && !$server->retrieveStatus()->isKillable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']),
|
||||
Action::make('kill')
|
||||
@@ -254,7 +266,7 @@ class ListServers extends ListRecords
|
||||
->color('danger')
|
||||
->icon('tabler-alert-square')
|
||||
->tooltip(trans('server/console.power_actions.kill_tooltip'))
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->authorize(fn (Server $server) => user()?->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn (Server $server) => $server->retrieveStatus()->isKillable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']),
|
||||
])
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Enums\EggFormat;
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
|
||||
class ExportEggAction extends Action
|
||||
{
|
||||
@@ -22,9 +22,15 @@ 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->authorize(fn () => auth()->user()->can('export egg'));
|
||||
$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);
|
||||
|
||||
@@ -38,17 +44,15 @@ class ExportEggAction extends Action
|
||||
|
||||
$this->modalFooterActionsAlignment(Alignment::Center);
|
||||
|
||||
$this->modalFooterActions([ //TODO: Close modal after clicking ->close() does not allow action to preform before closing modal
|
||||
$this->modalFooterActions([
|
||||
Action::make('json')
|
||||
->label(trans('admin/egg.export.as', ['format' => 'json']))
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::JSON);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
||||
->url(fn (Egg $egg) => route('api.application.eggs.eggs.export', ['egg' => $egg, 'format' => EggFormat::JSON->value]), true)
|
||||
->close(),
|
||||
Action::make('yaml')
|
||||
->label(trans('admin/egg.export.as', ['format' => 'yaml']))
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::YAML);
|
||||
}, 'egg-' . $egg->getKebabName() . '.yaml')),
|
||||
->url(fn (Egg $egg) => route('api.application.eggs.eggs.export', ['egg' => $egg, 'format' => EggFormat::YAML->value]), true)
|
||||
->close(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,15 +21,27 @@ 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();
|
||||
|
||||
$this->label(trans('filament-actions::export.modal.actions.export.label'));
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_READ, $server));
|
||||
$this->authorize(fn () => user()?->can(Permission::ACTION_SCHEDULE_READ, $server));
|
||||
|
||||
$this->action(fn (ScheduleExporterService $service, Schedule $schedule) => response()->streamDownload(function () use ($service, $schedule) {
|
||||
echo $service->handle($schedule);
|
||||
}, 'schedule-' . str($schedule->name)->kebab()->lower()->trim() . '.json'));
|
||||
}, 'schedule-' . str($schedule->name)->kebab()->lower()->trim() . '.json', [
|
||||
'Content-Type' => 'application/json',
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +36,13 @@ class ImportEggAction extends Action
|
||||
|
||||
$this->label(trans('filament-actions::import.modal.actions.import.label'));
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can('import egg'));
|
||||
$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 {
|
||||
$eggs = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
|
||||
|
||||
@@ -33,7 +33,7 @@ class ImportScheduleAction extends Action
|
||||
|
||||
$this->label(trans('filament-actions::import.modal.actions.import.label'));
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_CREATE, $server));
|
||||
$this->authorize(fn () => user()?->can(Permission::ACTION_SCHEDULE_CREATE, $server));
|
||||
|
||||
$this->schema([
|
||||
Tabs::make('Tabs')
|
||||
|
||||
@@ -15,19 +15,17 @@ class PreviewStartupAction extends Action
|
||||
return 'preview';
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return trans('server/startup.preview');
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label(fn (Get $get) => $get('previewing') ? trans('server/startup.disable_preview') : trans('server/startup.enable_preview'));
|
||||
|
||||
$this->action(function (Get $get, Set $set, Server $server) {
|
||||
$active = $get('previewing');
|
||||
$set('previewing', !$active);
|
||||
$set('startup', $active ? $server->startup : fn (Server $server, StartupCommandService $service) => $service->handle($server));
|
||||
$previewing = !$get('previewing');
|
||||
|
||||
$set('previewing', $previewing);
|
||||
$set('startup', !$previewing ? $server->startup : fn (Server $server, StartupCommandService $service) => $service->handle($server, $server->startup));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class RotateDatabasePasswordAction extends Action
|
||||
|
||||
$this->icon('tabler-refresh');
|
||||
|
||||
$this->authorize(fn (Database $database) => auth()->user()->can('update', $database));
|
||||
$this->authorize(fn (Database $database) => user()?->can('update', $database));
|
||||
|
||||
$this->modalHeading(trans('admin/databasehost.rotate_password'));
|
||||
|
||||
@@ -56,7 +56,7 @@ class RotateDatabasePasswordAction extends Action
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/databasehost.rotate_error'))
|
||||
->body(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin')) ? $exception->getMessage() : null)
|
||||
->body(fn () => user()?->canAccessPanel(Filament::getPanel('admin')) ? $exception->getMessage() : null)
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -59,7 +64,7 @@ class UpdateEggAction extends Action
|
||||
->send();
|
||||
});
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can('import egg'));
|
||||
$this->authorize(fn () => user()?->can('import egg'));
|
||||
|
||||
$this->visible(fn (Egg $egg) => cache()->get("eggs.$egg->uuid.update", false));
|
||||
}
|
||||
|
||||
@@ -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,13 +73,18 @@ 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();
|
||||
});
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can('import egg'));
|
||||
$this->authorize(fn () => user()?->can('import egg'));
|
||||
|
||||
$this->deselectRecordsAfterCompletion();
|
||||
}
|
||||
|
||||
109
app/Filament/Components/Actions/UpdateNodeAllocations.php
Normal file
109
app/Filament/Components/Actions/UpdateNodeAllocations.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
136
app/Filament/Components/Tables/Columns/Concerns/HasProgress.php
Normal file
136
app/Filament/Components/Tables/Columns/Concerns/HasProgress.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,6 @@ class DateTimeColumn extends TextColumn
|
||||
|
||||
public function getTimezone(): string
|
||||
{
|
||||
return auth()->user()->timezone ?? config('app.timezone', 'UTC');
|
||||
return user()->timezone ?? config('app.timezone', 'UTC');
|
||||
}
|
||||
}
|
||||
|
||||
162
app/Filament/Components/Tables/Columns/ProgressBarColumn.php
Normal file
162
app/Filament/Components/Tables/Columns/ProgressBarColumn.php
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -48,6 +47,7 @@ use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
/**
|
||||
* @method User getUser()
|
||||
@@ -128,11 +128,10 @@ class EditProfile extends BaseEditProfile
|
||||
->label(trans('profile.timezone'))
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->default('UTC')
|
||||
->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()
|
||||
@@ -142,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()
|
||||
@@ -151,14 +149,20 @@ class EditProfile extends BaseEditProfile
|
||||
->directory('avatars')
|
||||
->disk('public')
|
||||
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png')
|
||||
->hintAction(function (FileUpload $fileUpload) {
|
||||
->formatStateUsing(function (FileUpload $fileUpload) {
|
||||
$path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png';
|
||||
if ($fileUpload->getDisk()->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
})
|
||||
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
|
||||
if ($file instanceof TemporaryUploadedFile) {
|
||||
return $file->delete();
|
||||
}
|
||||
|
||||
return Action::make('remove_avatar')
|
||||
->icon('tabler-photo-minus')
|
||||
->iconButton()
|
||||
->hidden(fn () => !$fileUpload->getDisk()->exists($path))
|
||||
->action(fn () => $fileUpload->getDisk()->delete($path));
|
||||
if ($fileUpload->getDisk()->exists($file)) {
|
||||
return $fileUpload->getDisk()->delete($file);
|
||||
}
|
||||
}),
|
||||
]),
|
||||
Tab::make('oauth')
|
||||
@@ -181,10 +185,10 @@ class EditProfile extends BaseEditProfile
|
||||
->color(Color::hex($schema->getHexColor()))
|
||||
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
|
||||
if ($unlink) {
|
||||
$oauth = auth()->user()->oauth;
|
||||
$oauth = user()?->oauth;
|
||||
unset($oauth[$id]);
|
||||
|
||||
$updateService->handle(auth()->user(), ['oauth' => $oauth]);
|
||||
$updateService->handle(user(), ['oauth' => $oauth]);
|
||||
|
||||
$this->fillForm();
|
||||
|
||||
@@ -229,7 +233,7 @@ class EditProfile extends BaseEditProfile
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create')
|
||||
Action::make('create_api_key')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('description')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => 'api-keys::data::tab'], panel: 'app'))
|
||||
@@ -292,7 +296,13 @@ class EditProfile extends BaseEditProfile
|
||||
TextEntry::make('memo')
|
||||
->hiddenLabel()
|
||||
->state(fn (ApiKey $key) => $key->memo),
|
||||
]),
|
||||
])
|
||||
->visible(fn (User $user) => $user->apiKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_api_keys')
|
||||
->state(trans('profile.no_api_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->apiKeys()->exists()),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
@@ -312,7 +322,7 @@ class EditProfile extends BaseEditProfile
|
||||
->live(),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create')
|
||||
Action::make('create_ssh_key')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => empty($get('name')) || empty($get('public_key')))
|
||||
->successRedirectUrl(self::getUrl(['tab' => 'ssh-keys::data::tab'], panel: 'app'))
|
||||
@@ -381,7 +391,13 @@ class EditProfile extends BaseEditProfile
|
||||
TextEntry::make('fingerprint')
|
||||
->hiddenLabel()
|
||||
->state(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
|
||||
]),
|
||||
])
|
||||
->visible(fn (User $user) => $user->sshKeys()->exists()),
|
||||
|
||||
TextEntry::make('no_ssh_keys')
|
||||
->state(trans('profile.no_ssh_keys'))
|
||||
->hiddenLabel()
|
||||
->visible(fn (User $user) => !$user->sshKeys()->exists()),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
@@ -423,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()
|
||||
@@ -439,6 +455,7 @@ class EditProfile extends BaseEditProfile
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->live()
|
||||
->default(14),
|
||||
Select::make('console_font')
|
||||
->label(trans('profile.font'))
|
||||
@@ -463,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)
|
||||
@@ -535,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'),
|
||||
];
|
||||
|
||||
}
|
||||
@@ -565,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;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class Console extends Page
|
||||
{
|
||||
return [
|
||||
'server' => Filament::getTenant(),
|
||||
'user' => auth()->user(),
|
||||
'user' => user(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ class Console extends Page
|
||||
->label(trans('server/console.power_actions.start'))
|
||||
->color('primary')
|
||||
->icon('tabler-player-play-filled')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->authorize(fn (Server $server) => user()?->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->disabled(fn (Server $server) => $server->isInConflictState() || !$this->status->isStartable())
|
||||
->action(fn (Server $server) => $this->dispatch('setServerState', uuid: $server->uuid, state: 'start'))
|
||||
->size(Size::ExtraLarge),
|
||||
@@ -172,7 +172,7 @@ class Console extends Page
|
||||
->label(trans('server/console.power_actions.restart'))
|
||||
->color('gray')
|
||||
->icon('tabler-reload')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->authorize(fn (Server $server) => user()?->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->disabled(fn (Server $server) => $server->isInConflictState() || !$this->status->isRestartable())
|
||||
->action(fn (Server $server) => $this->dispatch('setServerState', uuid: $server->uuid, state: 'restart'))
|
||||
->size(Size::ExtraLarge),
|
||||
@@ -180,7 +180,7 @@ class Console extends Page
|
||||
->label(trans('server/console.power_actions.stop'))
|
||||
->color('danger')
|
||||
->icon('tabler-player-stop-filled')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->authorize(fn (Server $server) => user()?->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn () => !$this->status->isKillable())
|
||||
->disabled(fn (Server $server) => $server->isInConflictState() || !$this->status->isStoppable())
|
||||
->action(fn (Server $server) => $this->dispatch('setServerState', uuid: $server->uuid, state: 'stop'))
|
||||
@@ -191,7 +191,7 @@ class Console extends Page
|
||||
->icon('tabler-alert-square')
|
||||
->tooltip(trans('server/console.power_actions.kill_tooltip'))
|
||||
->requiresConfirmation()
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->authorize(fn (Server $server) => user()?->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn () => $this->status->isKillable())
|
||||
->disabled(fn (Server $server) => $server->isInConflictState() || !$this->status->isKillable())
|
||||
->action(fn (Server $server) => $this->dispatch('setServerState', uuid: $server->uuid, state: 'kill'))
|
||||
|
||||
@@ -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) => !auth()->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) => !auth()->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([
|
||||
@@ -153,7 +319,7 @@ class Settings extends ServerFormPage
|
||||
]),
|
||||
Fieldset::make(trans('server/setting.server_info.sftp.title'))
|
||||
->columnSpanFull()
|
||||
->hidden(fn (Server $server) => !auth()->user()->can(Permission::ACTION_FILE_SFTP, $server))
|
||||
->hidden(fn (Server $server) => !user()?->can(Permission::ACTION_FILE_SFTP, $server))
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
@@ -174,20 +340,20 @@ class Settings extends ServerFormPage
|
||||
->url(function (Server $server) {
|
||||
$fqdn = $server->node->daemon_sftp_alias ?? $server->node->fqdn;
|
||||
|
||||
return 'sftp://' . rawurlencode(auth()->user()->username) . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
return 'sftp://' . rawurlencode(user()?->username) . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
}),
|
||||
)
|
||||
->formatStateUsing(function (Server $server) {
|
||||
$fqdn = $server->node->daemon_sftp_alias ?? $server->node->fqdn;
|
||||
|
||||
return 'sftp://' . rawurlencode(auth()->user()->username) . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
return 'sftp://' . rawurlencode(user()?->username) . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
}),
|
||||
TextInput::make('username')
|
||||
->label(trans('server/setting.server_info.sftp.username'))
|
||||
->columnSpan(1)
|
||||
->copyable()
|
||||
->disabled()
|
||||
->formatStateUsing(fn (Server $server) => auth()->user()->username . '.' . $server->uuid_short),
|
||||
->formatStateUsing(fn (Server $server) => user()?->username . '.' . $server->uuid_short),
|
||||
TextEntry::make('password')
|
||||
->label(trans('server/setting.server_info.sftp.password'))
|
||||
->columnSpan(1)
|
||||
@@ -195,19 +361,19 @@ class Settings extends ServerFormPage
|
||||
]),
|
||||
]),
|
||||
Section::make(trans('server/setting.reinstall.title'))
|
||||
->hidden(fn (Server $server) => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
|
||||
->hidden(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
|
||||
->columnSpanFull()
|
||||
->footerActions([
|
||||
Action::make('reinstall')
|
||||
->label(trans('server/setting.reinstall.action'))
|
||||
->color('danger')
|
||||
->disabled(fn (Server $server) => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
|
||||
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
|
||||
->requiresConfirmation()
|
||||
->modalHeading(trans('server/setting.reinstall.modal'))
|
||||
->modalDescription(trans('server/setting.reinstall.modal_description'))
|
||||
->modalSubmitActionLabel(trans('server/setting.reinstall.yes'))
|
||||
->action(function (Server $server, ReinstallServerService $reinstallService) {
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403);
|
||||
abort_unless(user()?->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403);
|
||||
|
||||
try {
|
||||
$reinstallService->handle($server);
|
||||
@@ -246,7 +412,7 @@ class Settings extends ServerFormPage
|
||||
|
||||
public function updateName(string $name, Server $server): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server), 403);
|
||||
abort_unless(user()?->can(Permission::ACTION_SETTINGS_RENAME, $server), 403);
|
||||
|
||||
$original = $server->name;
|
||||
|
||||
@@ -277,7 +443,7 @@ class Settings extends ServerFormPage
|
||||
|
||||
public function updateDescription(string $description, Server $server): void
|
||||
{
|
||||
abort_unless(auth()->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;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user