mirror of
https://github.com/pelican-dev/panel.git
synced 2025-09-07 12:58:37 +02:00
Merge branch 'main' into lance/cloud
This commit is contained in:
commit
60b4cfe757
4
.github/workflows/lint.yaml
vendored
4
.github/workflows/lint.yaml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4]
|
||||
php: [ 8.2, 8.3, 8.4 ]
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@ -68,4 +68,4 @@ jobs:
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: PHPStan
|
||||
run: vendor/bin/phpstan --memory-limit=-1
|
||||
run: vendor/bin/phpstan --memory-limit=-1 --error-format=github
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,7 +1,6 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
@ -24,8 +23,7 @@ yarn-error.log
|
||||
/.vscode
|
||||
|
||||
public/assets/manifest.json
|
||||
/database/*.sqlite
|
||||
/database/*.sqlite-journal
|
||||
/database/*.sqlite*
|
||||
filament-monaco-editor/
|
||||
_ide_helper*
|
||||
/.phpstorm.meta.php
|
||||
|
25
Dockerfile
25
Dockerfile
@ -1,16 +1,9 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.13-labs
|
||||
# Pelican Production Dockerfile
|
||||
|
||||
|
||||
# For those who want to build this Dockerfile themselves, uncomment lines 6-12 and replace "localhost:5000/base-php:$TARGETARCH" on lines 17 and 67 with "base".
|
||||
|
||||
# FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine as base
|
||||
|
||||
# ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
# RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
|
||||
|
||||
# RUN rm /usr/local/bin/install-php-extensions
|
||||
##
|
||||
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
|
||||
##
|
||||
|
||||
# ================================
|
||||
# Stage 1-1: Composer Install
|
||||
@ -82,15 +75,15 @@ RUN chown root:www-data ./ \
|
||||
&& chmod 750 ./ \
|
||||
# Files should not have execute set, but directories need it
|
||||
&& find ./ -type d -exec chmod 750 {} \; \
|
||||
# Symlink to env/database path, as www-data won't be able to write to webroot
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
|
||||
# Symlinks for env, database, and avatars
|
||||
&& ln -s /pelican-data/.env ./.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& mkdir -p /pelican-data/storage \
|
||||
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage /var/www/html/storage/app/public/avatars \
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data /var/run/supervisord /etc/supercronic \
|
||||
# Finally allow www-data write permissions where necessary
|
||||
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
|
||||
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
|
||||
|
||||
|
112
Dockerfile.dev
Normal file
112
Dockerfile.dev
Normal file
@ -0,0 +1,112 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.13-labs
|
||||
# Pelican Development Dockerfile
|
||||
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
|
||||
|
||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
|
||||
|
||||
RUN rm /usr/local/bin/install-php-extensions
|
||||
|
||||
# ================================
|
||||
# Stage 1-1: Composer Install
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH base AS composer
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Copy bare minimum to install Composer dependencies
|
||||
COPY composer.json composer.lock ./
|
||||
|
||||
RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
|
||||
|
||||
# ================================
|
||||
# Stage 1-2: Yarn Install
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy bare minimum to install Yarn dependencies
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN yarn config set network-timeout 300000 \
|
||||
&& yarn install --frozen-lockfile
|
||||
|
||||
# ================================
|
||||
# Stage 2-1: Composer Optimize
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
|
||||
|
||||
# Copy full code to optimize autoload
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
|
||||
RUN composer dump-autoload --optimize
|
||||
|
||||
# ================================
|
||||
# Stage 2-2: Build Frontend Assets
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Copy full code
|
||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
|
||||
COPY --from=composer /build .
|
||||
|
||||
RUN yarn run build
|
||||
|
||||
# ================================
|
||||
# Stage 5: Build Final Application Image
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH base AS final
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install additional required libraries
|
||||
RUN apk update && apk add --no-cache \
|
||||
caddy ca-certificates supervisor supercronic
|
||||
|
||||
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
|
||||
|
||||
# Set permissions
|
||||
# First ensure all files are owned by root and restrict www-data to read access
|
||||
RUN chown root:www-data ./ \
|
||||
&& chmod 750 ./ \
|
||||
# Files should not have execute set, but directories need it
|
||||
&& find ./ -type d -exec chmod 750 {} \; \
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
|
||||
# Symlinks for env, database, and avatars
|
||||
&& ln -s /pelican-data/.env ./.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
|
||||
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
|
||||
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||
# Add Laravel scheduler to crontab
|
||||
COPY docker/crontab /etc/supercronic/crontab
|
||||
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
VOLUME /pelican-data
|
||||
|
||||
USER www-data
|
||||
|
||||
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
@ -18,6 +18,17 @@ class QueueWorkerServiceCommand extends Command
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (@file_exists('/.dockerenv')) {
|
||||
$result = Process::run('supervisorctl restart queue-worker');
|
||||
if ($result->failed()) {
|
||||
$this->error('Error restarting service: ' . $result->errorOutput());
|
||||
|
||||
return;
|
||||
}
|
||||
$this->line('Queue worker service file updated successfully.');
|
||||
|
||||
return;
|
||||
}
|
||||
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
|
||||
$path = '/etc/systemd/system/' . $serviceName . '.service';
|
||||
|
||||
|
@ -24,6 +24,7 @@ class MakeNodeCommand extends Command
|
||||
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
|
||||
{--uploadSize= : Enter the maximum upload filesize.}
|
||||
{--daemonListeningPort= : Enter the daemon listening port.}
|
||||
{--daemonConnectingPort= : Enter the daemon connecting port.}
|
||||
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
|
||||
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
|
||||
{--daemonBase= : Enter the base folder.}';
|
||||
@ -57,6 +58,7 @@ class MakeNodeCommand extends Command
|
||||
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1');
|
||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256');
|
||||
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080');
|
||||
$data['daemon_connect'] = $this->option('daemonConnectingPort') ?? $this->ask(trans('commands.make_node.daemonConnect'), '8080');
|
||||
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022');
|
||||
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
|
||||
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
|
||||
|
@ -7,7 +7,6 @@ use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneImagesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
|
||||
use App\Console\Commands\Schedule\ProcessRunnableCommand;
|
||||
use App\Jobs\NodeStatistics;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Webhook;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
@ -31,8 +30,11 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
|
||||
$schedule->command('cache:prune-stale-tags')->hourly();
|
||||
if (config('cache.default') === 'redis') {
|
||||
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
|
||||
// This only needs to run when using redis. anything else throws an error.
|
||||
$schedule->command('cache:prune-stale-tags')->hourly();
|
||||
}
|
||||
|
||||
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
|
||||
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
||||
@ -41,8 +43,6 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->command(PruneImagesCommand::class)->daily();
|
||||
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
|
||||
|
||||
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
|
||||
|
||||
if (config('backups.prune_age')) {
|
||||
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
|
||||
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();
|
||||
|
@ -14,4 +14,24 @@ enum RolePermissionModels: string
|
||||
case Server = 'server';
|
||||
case User = 'user';
|
||||
case Webhook = 'webhook';
|
||||
|
||||
public function viewAny(): string
|
||||
{
|
||||
return RolePermissionPrefixes::ViewAny->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function view(): string
|
||||
{
|
||||
return RolePermissionPrefixes::View->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function create(): string
|
||||
{
|
||||
return RolePermissionPrefixes::Create->value . ' ' . $this->value;
|
||||
}
|
||||
|
||||
public function update(): string
|
||||
{
|
||||
return RolePermissionPrefixes::Update->value . ' ' . $this->value;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
|
||||
class DirectLogin extends Event
|
||||
{
|
||||
public function __construct(public User $user, public bool $remember) {}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Auth;
|
||||
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FailedPasswordReset extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public string $ip, public string $email) {}
|
||||
}
|
@ -8,13 +8,17 @@ use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class GSLToken extends FeatureProvider
|
||||
{
|
||||
@ -27,14 +31,14 @@ class GSLToken extends FeatureProvider
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'gsl token expired',
|
||||
'account not found',
|
||||
'(gsl token expired)',
|
||||
'(account not found)',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gsltoken';
|
||||
return 'gsl_token';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
@ -43,7 +47,9 @@ class GSLToken extends FeatureProvider
|
||||
$server = Filament::getTenant();
|
||||
|
||||
/** @var ServerVariable $serverVariable */
|
||||
$serverVariable = $server->serverVariables()->where('env_variable', 'STEAM_ACC')->first();
|
||||
$serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
|
||||
$query->where('env_variable', 'STEAM_ACC');
|
||||
})->first();
|
||||
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
@ -52,9 +58,8 @@ class GSLToken extends FeatureProvider
|
||||
->modalSubmitActionLabel('Update GSL Token')
|
||||
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
|
||||
->form([
|
||||
Placeholder::make('java')
|
||||
->label('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.'),
|
||||
Placeholder::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.'))),
|
||||
TextInput::make('gsltoken')
|
||||
->label('GSL Token')
|
||||
->rules([
|
||||
@ -102,13 +107,13 @@ class GSLToken extends FeatureProvider
|
||||
|
||||
Notification::make()
|
||||
->title('GSL Token updated')
|
||||
->body('Restart the server to use the new token.')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Error')
|
||||
->body($e->getMessage())
|
||||
->title('Could not update GSL Token')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
@ -25,10 +26,11 @@ class JavaVersion extends FeatureProvider
|
||||
{
|
||||
return [
|
||||
'java.lang.UnsupportedClassVersionError',
|
||||
'minecraft 1.17 requires running the server with java 16 or above',
|
||||
'minecraft 1.18 requires running the server with java 17 or above',
|
||||
'unsupported major.minor version',
|
||||
'has been compiled by a more recent version of the java runtime',
|
||||
'minecraft 1.17 requires running the server with java 16 or above',
|
||||
'minecraft 1.18 requires running the server with java 17 or above',
|
||||
'minecraft 1.19 requires running the server with java 17 or above',
|
||||
];
|
||||
}
|
||||
|
||||
@ -73,17 +75,18 @@ class JavaVersion extends FeatureProvider
|
||||
->property(['old' => $original, 'new' => $new])
|
||||
->log();
|
||||
}
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('Docker image updated')
|
||||
->body('Restart the server to use the new image.')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Error')
|
||||
->body($e->getMessage())
|
||||
->title('Could not update docker image')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ class MinecraftEula extends FeatureProvider
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'You need to agree to the EULA in order to run the server',
|
||||
'you need to agree to the eula in order to run the server',
|
||||
];
|
||||
}
|
||||
|
||||
@ -38,31 +38,30 @@ class MinecraftEula extends FeatureProvider
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Minecraft EULA')
|
||||
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>')))
|
||||
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
|
||||
->modalSubmitActionLabel('I Accept')
|
||||
->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
|
||||
try {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
$content = $fileRepository->setServer($server)->getContent('eula.txt');
|
||||
$content = preg_replace('/(eula=)false/', '\1true', $content);
|
||||
$fileRepository->setServer($server)->putContent('eula.txt', $content);
|
||||
|
||||
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('Docker image updated')
|
||||
->body('Restart the server.')
|
||||
->title('Minecraft EULA accepted')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Error')
|
||||
->body($e->getMessage())
|
||||
->title('Could not accept Minecraft EULA')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
|
@ -13,6 +13,7 @@ use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Section;
|
||||
@ -33,6 +34,7 @@ use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Http\Client\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||
use Illuminate\Support\Str;
|
||||
@ -136,8 +138,7 @@ class Settings extends Page implements HasForms
|
||||
->placeholder('/pelican.ico'),
|
||||
]),
|
||||
Group::make()
|
||||
->columnSpan(2)
|
||||
->columns(4)
|
||||
->columns(2)
|
||||
->schema([
|
||||
Toggle::make('APP_DEBUG')
|
||||
->label(trans('admin/setting.general.debug_mode'))
|
||||
@ -159,6 +160,10 @@ class Settings extends Page implements HasForms
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
|
||||
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
|
||||
]),
|
||||
Group::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('FILAMENT_AVATAR_PROVIDER')
|
||||
->label(trans('admin/setting.general.avatar_provider'))
|
||||
->native(false)
|
||||
@ -197,12 +202,18 @@ class Settings extends Page implements HasForms
|
||||
->formatStateUsing(fn ($state): int => (int) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
|
||||
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
|
||||
Select::make('FILAMENT_WIDTH')
|
||||
->label(trans('admin/setting.general.display_width'))
|
||||
->native(false)
|
||||
->options(MaxWidth::class)
|
||||
->selectablePlaceholder(false)
|
||||
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
|
||||
TagsInput::make('TRUSTED_PROXIES')
|
||||
->label(trans('admin/setting.general.trusted_proxies'))
|
||||
->separator()
|
||||
->splitKeys(['Tab', ' '])
|
||||
->placeholder(trans('admin/setting.general.trusted_proxies_help'))
|
||||
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
|
||||
->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies')))))
|
||||
->hintActions([
|
||||
FormAction::make('clear')
|
||||
->label(trans('admin/setting.general.clear'))
|
||||
@ -237,12 +248,6 @@ class Settings extends Page implements HasForms
|
||||
$set('TRUSTED_PROXIES', $ips->values()->all());
|
||||
}),
|
||||
]),
|
||||
Select::make('FILAMENT_WIDTH')
|
||||
->label(trans('admin/setting.general.display_width'))
|
||||
->native(false)
|
||||
->options(MaxWidth::class)
|
||||
->selectablePlaceholder(false)
|
||||
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
|
||||
];
|
||||
}
|
||||
|
||||
@ -625,7 +630,6 @@ class Settings extends Page implements HasForms
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
|
||||
@ -636,7 +640,6 @@ class Settings extends Page implements HasForms
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
@ -724,10 +727,17 @@ class Settings extends Page implements HasForms
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->columnSpan(1)
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
|
||||
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
|
||||
FileUpload::make('ConsoleFonts')
|
||||
->hint(trans('admin/setting.misc.server.console_font_hint'))
|
||||
->label(trans('admin/setting.misc.server.console_font_upload'))
|
||||
->directory('fonts')
|
||||
->columnSpan(1)
|
||||
->maxFiles(1)
|
||||
->preserveFilenames(),
|
||||
]),
|
||||
Section::make(trans('admin/setting.misc.webhook.title'))
|
||||
->description(trans('admin/setting.misc.webhook.helper'))
|
||||
@ -756,6 +766,7 @@ class Settings extends Page implements HasForms
|
||||
{
|
||||
try {
|
||||
$data = $this->form->getState();
|
||||
unset($data['ConsoleFonts']);
|
||||
|
||||
// Convert bools to a string, so they are correctly written to the .env file
|
||||
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data);
|
||||
|
@ -79,7 +79,7 @@ class ApiKeyResource extends Resource
|
||||
TextColumn::make('user.username')
|
||||
->label(trans('admin/apikey.table.created_by'))
|
||||
->icon('tabler-user')
|
||||
->url(fn (ApiKey $apiKey) => auth()->user()->can('update user', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
|
||||
->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
|
@ -16,6 +16,7 @@ use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DatabaseHostResource extends Resource
|
||||
{
|
||||
@ -27,7 +28,7 @@ class DatabaseHostResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
@ -144,7 +145,7 @@ class DatabaseHostResource extends Resource
|
||||
->preload()
|
||||
->helperText(trans('admin/databasehost.linked_nodes_help'))
|
||||
->label(trans('admin/databasehost.linked_nodes'))
|
||||
->relationship('nodes', 'name'),
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@ -158,4 +159,15 @@ class DatabaseHostResource extends Resource
|
||||
'edit' => Pages\EditDatabaseHost::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->where(function (Builder $query) {
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
@ -145,7 +146,7 @@ class CreateDatabaseHost extends CreateRecord
|
||||
->preload()
|
||||
->helperText(trans('admin/databasehost.linked_nodes_help'))
|
||||
->label(trans('admin/databasehost.linked_nodes'))
|
||||
->relationship('nodes', 'name'),
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
@ -71,10 +71,10 @@ class DatabasesRelationManager extends RelationManager
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete', $database)),
|
||||
ViewAction::make()
|
||||
->color('primary')
|
||||
->hidden(fn () => !auth()->user()->can('viewList database')),
|
||||
->hidden(fn () => !auth()->user()->can('viewAny', Database::class)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class MountResource extends Resource
|
||||
{
|
||||
@ -44,7 +45,7 @@ class MountResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
@ -75,7 +76,7 @@ class MountResource extends Resource
|
||||
->badge()
|
||||
->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing')
|
||||
->color(fn ($state) => $state ? 'success' : 'warning')
|
||||
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writeable')),
|
||||
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
@ -147,7 +148,7 @@ class MountResource extends Resource
|
||||
->preload(),
|
||||
Select::make('nodes')->multiple()
|
||||
->label(trans('admin/mount.nodes'))
|
||||
->relationship('nodes', 'name')
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->searchable(['name', 'fqdn'])
|
||||
->preload(),
|
||||
]),
|
||||
@ -170,4 +171,15 @@ class MountResource extends Resource
|
||||
'edit' => Pages\EditMount::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->where(function (Builder $query) {
|
||||
return $query->whereHas('nodes', function (Builder $query) {
|
||||
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
})->orDoesntHave('nodes');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use App\Filament\Admin\Resources\NodeResource\Pages;
|
||||
use App\Filament\Admin\Resources\NodeResource\RelationManagers;
|
||||
use App\Models\Node;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class NodeResource extends Resource
|
||||
{
|
||||
@ -37,7 +38,7 @@ class NodeResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
@ -56,4 +57,11 @@ class NodeResource extends Resource
|
||||
'edit' => Pages\EditNode::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use App\Services\Api\KeyCreationService;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
@ -54,14 +55,14 @@ class CreateNode extends CreateRecord
|
||||
{
|
||||
$key = ApiKey::query()
|
||||
->where('key_type', ApiKey::TYPE_APPLICATION)
|
||||
->whereJsonContains('permissions->' . Node::RESOURCE_NAME, AdminAcl::READ|AdminAcl::WRITE)
|
||||
->whereJsonContains('permissions->' . Node::RESOURCE_NAME, AdminAcl::READ | AdminAcl::WRITE)
|
||||
->first();
|
||||
|
||||
if (!$key) {
|
||||
$key = $this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([
|
||||
'memo' => 'Automatically generated node cloud key.',
|
||||
'user_id' => auth()->user()->id,
|
||||
'permissions' => [Node::RESOURCE_NAME => AdminAcl::READ|AdminAcl::WRITE],
|
||||
'permissions' => [Node::RESOURCE_NAME => AdminAcl::READ | AdminAcl::WRITE],
|
||||
]);
|
||||
}
|
||||
|
||||
@ -83,8 +84,7 @@ class CreateNode extends CreateRecord
|
||||
It looks like there was a problem communicating with Pelican Cloud!
|
||||
Please make a ticket by <a class='underline text-blue-400' href='https://hub.pelican.dev/tickets?code=$code'>clicking here</a>.
|
||||
"))
|
||||
->send()
|
||||
;
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
@ -106,8 +106,7 @@ class CreateNode extends CreateRecord
|
||||
->schema([
|
||||
View::make('filament.admin.nodes.config.left'),
|
||||
View::make('filament.admin.nodes.config.right'),
|
||||
])
|
||||
,
|
||||
]),
|
||||
Step::make('basic')
|
||||
->label(trans('admin/node.tabs.basic_settings'))
|
||||
->icon('tabler-server')
|
||||
@ -202,15 +201,10 @@ class CreateNode extends CreateRecord
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('admin/node.port'))
|
||||
->helperText(trans('admin/node.port_help'))
|
||||
TextInput::make('daemon_connect')
|
||||
->columnSpan(1)
|
||||
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
|
||||
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
@ -228,14 +222,15 @@ class CreateNode extends CreateRecord
|
||||
->required()
|
||||
->maxLength(100),
|
||||
|
||||
ToggleButtons::make('scheme')
|
||||
Hidden::make('scheme')
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
|
||||
Hidden::make('behind_proxy')
|
||||
->default(false),
|
||||
|
||||
ToggleButtons::make('connection')
|
||||
->label(trans('admin/node.ssl'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->columnSpan(1)
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
@ -248,20 +243,43 @@ class CreateNode extends CreateRecord
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
'https_proxy' => 'HTTPS with (reverse) proxy',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
'https_proxy' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
'https_proxy' => 'tabler-shield-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http')
|
||||
->live()
|
||||
->dehydrated(false)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$set('scheme', $state === 'http' ? 'http' : 'https');
|
||||
$set('behind_proxy', $state === 'https_proxy');
|
||||
|
||||
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
|
||||
$set('daemon_listen', 8080);
|
||||
}),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
]),
|
||||
Step::make('advanced')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
@ -486,4 +504,13 @@ class CreateNode extends CreateRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (!$data['behind_proxy']) {
|
||||
$data['daemon_listen'] = $data['daemon_connect'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions as FormActions;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
@ -180,10 +181,10 @@ class EditNode extends EditRecord
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan(1),
|
||||
TextInput::make('daemon_listen')
|
||||
TextInput::make('daemon_connect')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.port'))
|
||||
->helperText(trans('admin/node.port_help'))
|
||||
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
|
||||
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
@ -199,7 +200,9 @@ class EditNode extends EditRecord
|
||||
])
|
||||
->required()
|
||||
->maxLength(100),
|
||||
ToggleButtons::make('scheme')
|
||||
Hidden::make('scheme'),
|
||||
Hidden::make('behind_proxy'),
|
||||
ToggleButtons::make('connection')
|
||||
->label(trans('admin/node.ssl'))
|
||||
->columnSpan(1)
|
||||
->inline()
|
||||
@ -214,20 +217,43 @@ class EditNode extends EditRecord
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
'https_proxy' => 'HTTPS with (reverse) proxy',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
'https_proxy' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
'https_proxy' => 'tabler-shield-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||
->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https'))
|
||||
->live()
|
||||
->dehydrated(false)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$set('scheme', $state === 'http' ? 'http' : 'https');
|
||||
$set('behind_proxy', $state === 'https_proxy');
|
||||
|
||||
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
|
||||
$set('daemon_listen', 8080);
|
||||
}),
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
]),
|
||||
Tab::make('adv')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
->columns([
|
||||
@ -614,6 +640,15 @@ class EditNode extends EditRecord
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if (!$data['behind_proxy']) {
|
||||
$data['daemon_listen'] = $data['daemon_connect'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->fillForm();
|
||||
|
@ -12,8 +12,7 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
@ -32,18 +31,12 @@ class AllocationsRelationManager extends RelationManager
|
||||
public function setTitle(): string
|
||||
{
|
||||
return trans('admin/server.allocations');
|
||||
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('ip')
|
||||
|
||||
// Non Primary Allocations
|
||||
// ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
|
||||
|
||||
// All assigned allocations
|
||||
->recordTitleAttribute('address')
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||
->paginationPageOptions(['10', '20', '50', '100', '200', '500'])
|
||||
->searchable()
|
||||
@ -72,14 +65,14 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(trans('admin/node.table.ip')),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('create new allocation')
|
||||
Action::make('create new allocation')
|
||||
->label(trans('admin/node.create_allocation'))
|
||||
->form(fn () => [
|
||||
Select::make('allocation_ip')
|
||||
->options(collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->label(trans('admin/node.ip_address'))
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->ip()
|
||||
->helperText(trans('admin/node.ip_help'))
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
->live()
|
||||
@ -96,19 +89,15 @@ class AllocationsRelationManager extends RelationManager
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->disabled(fn (Get $get) => empty($get('allocation_ip')))
|
||||
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
|
||||
CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip')))
|
||||
)
|
||||
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip'))))
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('update node')),
|
||||
]),
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Filament\Admin\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
@ -16,22 +15,34 @@ class NodeCpuChart extends ChartWidget
|
||||
|
||||
public Node $node;
|
||||
|
||||
/**
|
||||
* @var array<int, array{cpu: string, timestamp: string}>
|
||||
*/
|
||||
protected array $cpuHistory = [];
|
||||
|
||||
protected int $threads = 0;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$threads = $this->node->systemInformation()['cpu_count'] ?? 0;
|
||||
$sessionKey = "node_stats.{$this->node->id}";
|
||||
|
||||
$cpu = collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => round($value * $threads, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
$data = $this->node->statistics();
|
||||
|
||||
$this->threads = session("{$sessionKey}.threads", $this->node->systemInformation()['cpu_count'] ?? 0);
|
||||
|
||||
$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'),
|
||||
];
|
||||
|
||||
$this->cpuHistory = array_slice($this->cpuHistory, -60);
|
||||
session()->put("{$sessionKey}.cpu_history", $this->cpuHistory);
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($cpu, 'cpu'),
|
||||
'data' => array_column($this->cpuHistory, 'cpu'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
@ -39,7 +50,7 @@ class NodeCpuChart extends ChartWidget
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($cpu, 'timestamp'),
|
||||
'labels' => array_column($this->cpuHistory, 'timestamp'),
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
@ -69,10 +80,10 @@ class NodeCpuChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$threads = $this->node->systemInformation()['cpu_count'] ?? 0;
|
||||
$data = array_slice(end($this->cpuHistory), -60);
|
||||
|
||||
$cpu = Number::format(collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))->last() * $threads, maxPrecision: 2, locale: auth()->user()->language);
|
||||
$max = Number::format($threads * 100, locale: auth()->user()->language);
|
||||
$cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language);
|
||||
$max = Number::format($this->threads * 100, locale: auth()->user()->language);
|
||||
|
||||
return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]);
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Filament\Admin\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
@ -16,19 +15,36 @@ class NodeMemoryChart extends ChartWidget
|
||||
|
||||
public Node $node;
|
||||
|
||||
/**
|
||||
* @var array<int, array{memory: string, timestamp: string}>
|
||||
*/
|
||||
protected array $memoryHistory = [];
|
||||
|
||||
protected int $totalMemory = 0;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$memUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
$sessionKey = "node_stats.{$this->node->id}";
|
||||
|
||||
$data = $this->node->statistics();
|
||||
|
||||
$this->totalMemory = session("{$sessionKey}.total_memory", $data['memory_total']);
|
||||
|
||||
$this->memoryHistory = session("{$sessionKey}.memory_history", []);
|
||||
$this->memoryHistory[] = [
|
||||
'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'),
|
||||
];
|
||||
|
||||
$this->memoryHistory = array_slice($this->memoryHistory, -60);
|
||||
session()->put("{$sessionKey}.memory_history", $this->memoryHistory);
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($memUsed, 'memory'),
|
||||
'data' => array_column($this->memoryHistory, 'memory'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
@ -36,7 +52,7 @@ class NodeMemoryChart extends ChartWidget
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($memUsed, 'timestamp'),
|
||||
'labels' => array_column($this->memoryHistory, 'timestamp'),
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
@ -66,16 +82,15 @@ class NodeMemoryChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$latestMemoryUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->last();
|
||||
$totalMemory = collect(cache()->get("nodes.{$this->node->id}.memory_total"))->last();
|
||||
$latestMemoryUsed = array_slice(end($this->memoryHistory), -60);
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
? Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
? Number::format($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
@ -69,6 +70,11 @@ class RoleResource extends Resource
|
||||
->badge()
|
||||
->counts('permissions')
|
||||
->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? trans('admin/role.all') : $state),
|
||||
TextColumn::make('nodes.name')
|
||||
->icon('tabler-server-2')
|
||||
->label(trans('admin/role.nodes'))
|
||||
->badge()
|
||||
->placeholder(trans('admin/role.all')),
|
||||
TextColumn::make('users_count')
|
||||
->label(trans('admin/role.users'))
|
||||
->counts('users')
|
||||
@ -125,6 +131,14 @@ class RoleResource extends Resource
|
||||
->label(trans('admin/role.permissions'))
|
||||
->content(trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]))
|
||||
->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
Select::make('nodes')
|
||||
->label(trans('admin/role.nodes'))
|
||||
->multiple()
|
||||
->relationship('nodes', 'name')
|
||||
->searchable(['name', 'fqdn'])
|
||||
->preload()
|
||||
->hint(trans('admin/role.nodes_hint'))
|
||||
->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,12 @@
|
||||
namespace App\Filament\Admin\Resources;
|
||||
|
||||
use App\Filament\Admin\Resources\ServerResource\Pages;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ServerResource extends Resource
|
||||
{
|
||||
@ -36,7 +40,30 @@ class ServerResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
}
|
||||
|
||||
public static function getMountCheckboxList(Get $get): CheckboxList
|
||||
{
|
||||
$allowedMounts = Mount::all();
|
||||
$node = $get('node_id');
|
||||
$egg = $get('egg_id');
|
||||
|
||||
if ($node && $egg) {
|
||||
$allowedMounts = $allowedMounts->filter(fn (Mount $mount) => ($mount->nodes->isEmpty() || $mount->nodes->contains($node)) &&
|
||||
($mount->eggs->isEmpty() || $mount->eggs->contains($egg))
|
||||
);
|
||||
}
|
||||
|
||||
return CheckboxList::make('mounts')
|
||||
->label('')
|
||||
->relationship('mounts')
|
||||
->live()
|
||||
->options(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
|
||||
->descriptions(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
|
||||
->helperText(fn () => $allowedMounts->isEmpty() ? trans('admin/server.no_mounts') : null)
|
||||
->bulkToggleable()
|
||||
->columnSpanFull();
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@ -47,4 +74,11 @@ class ServerResource extends Resource
|
||||
'edit' => Pages\EditServer::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$query = parent::getEloquentQuery();
|
||||
|
||||
return $query->whereIn('node_id', auth()->user()->accessibleNodes()->pluck('id'));
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ use Closure;
|
||||
use Exception;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
@ -109,14 +108,20 @@ class CreateServer extends CreateRecord
|
||||
->disabledOn('edit')
|
||||
->prefixIcon('tabler-server-2')
|
||||
->selectablePlaceholder(false)
|
||||
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
|
||||
->default(function () {
|
||||
/** @var ?Node $latestNode */
|
||||
$latestNode = auth()->user()->accessibleNodes()->latest()->first();
|
||||
$this->node = $latestNode;
|
||||
|
||||
return $this->node?->id;
|
||||
})
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
])
|
||||
->live()
|
||||
->relationship('node', 'name')
|
||||
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->searchable()
|
||||
->preload()
|
||||
->afterStateUpdated(function (Set $set, $state) {
|
||||
@ -139,6 +144,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)))
|
||||
->createOptionForm([
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/user.username'))
|
||||
@ -183,10 +189,7 @@ class CreateServer extends CreateRecord
|
||||
$set('allocation_additional', null);
|
||||
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
|
||||
})
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
|
||||
->placeholder(function (Get $get) {
|
||||
$node = Node::find($get('node_id'));
|
||||
|
||||
@ -203,6 +206,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')))))
|
||||
->createOptionForm(function (Get $get) {
|
||||
$getPage = $get;
|
||||
|
||||
@ -212,7 +216,7 @@ class CreateServer extends CreateRecord
|
||||
->label(trans('admin/server.ip_address'))->inlineLabel()
|
||||
->helperText(trans('admin/server.ip_address_helper'))
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
->ipv4()
|
||||
->ip()
|
||||
->live()
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
@ -263,10 +267,7 @@ class CreateServer extends CreateRecord
|
||||
->columnSpan(2)
|
||||
->disabled(fn (Get $get) => $get('../../node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
|
||||
->placeholder(trans('admin/server.select_additional'))
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->relationship(
|
||||
@ -744,7 +745,7 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 4,
|
||||
])
|
||||
->columnSpan(6)
|
||||
->schema([
|
||||
->schema(fn (Get $get) => [
|
||||
Select::make('select_image')
|
||||
->label(trans('admin/server.image_name'))
|
||||
->live()
|
||||
@ -798,14 +799,7 @@ class CreateServer extends CreateRecord
|
||||
->valueLabel(trans('admin/server.description'))
|
||||
->columnSpanFull(),
|
||||
|
||||
CheckboxList::make('mounts')
|
||||
->label('Mounts')
|
||||
->live()
|
||||
->relationship('mounts')
|
||||
->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
|
||||
->descriptions(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]) ?? [])
|
||||
->helperText(fn () => $this->node?->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
|
||||
->columnSpanFull(),
|
||||
ServerResource::getMountCheckboxList($get),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Filament\Admin\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Enums\SuspendAction;
|
||||
use App\Filament\Admin\Resources\ServerResource;
|
||||
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
|
||||
@ -13,7 +13,6 @@ use App\Models\Allocation;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
@ -33,7 +32,6 @@ use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions as FormActions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
@ -53,6 +51,7 @@ use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Arr;
|
||||
@ -137,7 +136,39 @@ class EditServer extends EditRecord
|
||||
'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'))
|
||||
->form([
|
||||
MonacoEditor::make('logs')
|
||||
->hiddenLabel()
|
||||
->placeholderText(trans('admin/server.no_log'))
|
||||
->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 '';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->language('shell')
|
||||
->view('filament.plugins.monaco-editor-logs'),
|
||||
])
|
||||
),
|
||||
|
||||
Textarea::make('description')
|
||||
->label(trans('admin/server.description'))
|
||||
@ -177,7 +208,7 @@ class EditServer extends EditRecord
|
||||
->maxLength(255),
|
||||
Select::make('node_id')
|
||||
->label(trans('admin/server.node'))
|
||||
->relationship('node', 'name')
|
||||
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 1,
|
||||
@ -651,17 +682,11 @@ class EditServer extends EditRecord
|
||||
]),
|
||||
Tab::make(trans('admin/server.mounts'))
|
||||
->icon('tabler-layers-linked')
|
||||
->schema([
|
||||
CheckboxList::make('mounts')
|
||||
->label('')
|
||||
->relationship('mounts')
|
||||
->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
|
||||
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
|
||||
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : trans('admin/server.no_mounts'))
|
||||
->columnSpanFull(),
|
||||
->schema(fn (Get $get) => [
|
||||
ServerResource::getMountCheckboxList($get),
|
||||
]),
|
||||
Tab::make(trans('admin/server.databases'))
|
||||
->hidden(fn () => !auth()->user()->can('viewList database'))
|
||||
->hidden(fn () => !auth()->user()->can('viewAny', Database::class))
|
||||
->icon('tabler-database')
|
||||
->columns(4)
|
||||
->schema([
|
||||
@ -685,14 +710,14 @@ class EditServer extends EditRecord
|
||||
->hintAction(
|
||||
Action::make('Delete')
|
||||
->label(trans('filament-actions::delete.single.modal.actions.delete.label'))
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database))
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete', $database))
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->modalIcon('tabler-database-x')
|
||||
->modalHeading(trans('admin/server.delete_db_heading'))
|
||||
->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
|
||||
->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?')
|
||||
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
|
||||
->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')]))
|
||||
->action(function (DatabaseManagementService $databaseManagementService, $record) {
|
||||
$databaseManagementService->delete($record);
|
||||
$this->fillForm();
|
||||
@ -738,7 +763,7 @@ class EditServer extends EditRecord
|
||||
->columnSpan(4),
|
||||
FormActions::make([
|
||||
Action::make('createDatabase')
|
||||
->authorize(fn () => auth()->user()->can('create database'))
|
||||
->authorize(fn () => auth()->user()->can('create', Database::class))
|
||||
->disabled(fn () => DatabaseHost::query()->count() < 1)
|
||||
->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database'))
|
||||
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
|
||||
@ -808,12 +833,12 @@ class EditServer extends EditRecord
|
||||
Action::make('toggleInstall')
|
||||
->label(trans('admin/server.toggle_install'))
|
||||
->disabled(fn (Server $server) => $server->isSuspended())
|
||||
->modal(fn (Server $server) => $server->status === ServerState::InstallFailed)
|
||||
->modal(fn (Server $server) => $server->isFailedInstall())
|
||||
->modalHeading(trans('admin/server.toggle_install_failed_header'))
|
||||
->modalDescription(trans('admin/server.toggle_install_failed_desc'))
|
||||
->modalSubmitActionLabel(trans('admin/server.reinstall'))
|
||||
->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) {
|
||||
if ($server->status === ServerState::InstallFailed) {
|
||||
if ($server->isFailedInstall()) {
|
||||
try {
|
||||
$reinstallService->handle($server);
|
||||
|
||||
@ -826,7 +851,7 @@ class EditServer extends EditRecord
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.reinstall_failed'))
|
||||
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
|
||||
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
@ -875,7 +900,7 @@ class EditServer extends EditRecord
|
||||
Notification::make()
|
||||
->warning()
|
||||
->title(trans('admin/server.notifications.server_suspension'))
|
||||
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
|
||||
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
@ -897,7 +922,7 @@ class EditServer extends EditRecord
|
||||
Notification::make()
|
||||
->warning()
|
||||
->title(trans('admin/server.notifications.server_suspension'))
|
||||
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
|
||||
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
@ -962,7 +987,7 @@ class EditServer extends EditRecord
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.reinstall_failed'))
|
||||
->body(trans('admin/server.error_connecting', ['node' => $server->node->name]))
|
||||
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
@ -1040,7 +1065,7 @@ class EditServer extends EditRecord
|
||||
}
|
||||
})
|
||||
->hidden(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)),
|
||||
Actions\Action::make('ForceDelete')
|
||||
->color('danger')
|
||||
->label(trans('filament-actions::force-delete.single.label'))
|
||||
@ -1057,7 +1082,7 @@ class EditServer extends EditRecord
|
||||
}
|
||||
})
|
||||
->visible(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)),
|
||||
Actions\Action::make('console')
|
||||
->label(trans('admin/server.console'))
|
||||
->icon('tabler-terminal')
|
||||
@ -1078,7 +1103,7 @@ class EditServer extends EditRecord
|
||||
$data['description'] = '';
|
||||
}
|
||||
|
||||
unset($data['docker'], $data['status']);
|
||||
unset($data['docker'], $data['status'], $data['allocation_id']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
@ -68,13 +68,13 @@ class ListServers extends ListRecords
|
||||
->searchable(),
|
||||
SelectColumn::make('allocation_id')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->hidden(!auth()->user()->can('update server'))
|
||||
->hidden(!auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
TextColumn::make('allocation_id_readonly')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->hidden(auth()->user()->can('update server'))
|
||||
->hidden(auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->state(fn (Server $server) => $server->allocation->address),
|
||||
TextColumn::make('image')->hidden(),
|
||||
TextColumn::make('backups_count')
|
||||
|
@ -16,6 +16,7 @@ use Filament\Support\Exceptions\Halt;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\AssociateAction;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DissociateAction;
|
||||
use Filament\Tables\Actions\DissociateBulkAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
@ -34,15 +35,18 @@ class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
return $table
|
||||
->selectCurrentPageOnly()
|
||||
->recordTitleAttribute('ip')
|
||||
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
|
||||
->recordTitleAttribute('address')
|
||||
->recordTitle(fn (Allocation $allocation) => $allocation->address)
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
|
||||
->inverseRelationship('server')
|
||||
->heading(trans('admin/server.allocations'))
|
||||
->columns([
|
||||
TextColumn::make('ip')->label(trans('admin/server.ip_address')),
|
||||
TextColumn::make('port')->label(trans('admin/server.port')),
|
||||
TextInputColumn::make('ip_alias')->label(trans('admin/server.alias')),
|
||||
TextColumn::make('ip')
|
||||
->label(trans('admin/server.ip_address')),
|
||||
TextColumn::make('port')
|
||||
->label(trans('admin/server.port')),
|
||||
TextInputColumn::make('ip_alias')
|
||||
->label(trans('admin/server.alias')),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
@ -58,8 +62,11 @@ class AllocationsRelationManager extends RelationManager
|
||||
])
|
||||
->actions([
|
||||
Action::make('make-primary')
|
||||
->label(trans('admin/server.make_primary'))
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : trans('admin/server.make_primary')),
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
|
||||
DissociateAction::make()
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make()->label(trans('admin/server.create_allocation'))
|
||||
@ -69,7 +76,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->options(collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->label(trans('admin/server.ip_address'))
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->ip()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
->required(),
|
||||
@ -85,9 +92,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->disabled(fn (Get $get) => empty($get('allocation_ip')))
|
||||
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
|
||||
CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))
|
||||
)
|
||||
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))))
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
|
@ -6,15 +6,22 @@ use App\Enums\ServerResourceType;
|
||||
use App\Filament\App\Resources\ServerResource;
|
||||
use App\Filament\Components\Tables\Columns\ServerEntryColumn;
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use AymanAlhattami\FilamentContextMenu\Columns\ContextMenuTextColumn;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Columns\ColumnGroup;
|
||||
use Filament\Tables\Columns\Layout\Stack;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class ListServers extends ListRecords
|
||||
{
|
||||
@ -24,12 +31,51 @@ class ListServers extends ListRecords
|
||||
|
||||
public const WARNING_THRESHOLD = 0.7;
|
||||
|
||||
private DaemonPowerRepository $daemonPowerRepository;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->daemonPowerRepository = new DaemonPowerRepository();
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
$baseQuery = auth()->user()->accessibleServers();
|
||||
|
||||
$menuOptions = function (Server $server) {
|
||||
$status = $server->retrieveStatus();
|
||||
|
||||
return [
|
||||
Action::make('start')
|
||||
->color('primary')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->visible(fn () => $status->isStartable())
|
||||
->dispatch('powerAction', ['server' => $server, 'action' => 'start'])
|
||||
->icon('tabler-player-play-filled'),
|
||||
Action::make('restart')
|
||||
->color('gray')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->visible(fn () => $status->isRestartable())
|
||||
->dispatch('powerAction', ['server' => $server, 'action' => 'restart'])
|
||||
->icon('tabler-refresh'),
|
||||
Action::make('stop')
|
||||
->color('danger')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn () => $status->isStoppable())
|
||||
->dispatch('powerAction', ['server' => $server, 'action' => 'stop'])
|
||||
->icon('tabler-player-stop-filled'),
|
||||
Action::make('kill')
|
||||
->color('danger')
|
||||
->tooltip('This can result in data corruption and/or data loss!')
|
||||
->dispatch('powerAction', ['server' => $server, 'action' => 'kill'])
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn () => $status->isKillable())
|
||||
->icon('tabler-alert-square'),
|
||||
];
|
||||
};
|
||||
|
||||
$viewOne = [
|
||||
TextColumn::make('condition')
|
||||
ContextMenuTextColumn::make('condition')
|
||||
->label('')
|
||||
->default('unknown')
|
||||
->wrap()
|
||||
@ -37,20 +83,24 @@ class ListServers extends ListRecords
|
||||
->alignCenter()
|
||||
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
|
||||
->icon(fn (Server $server) => $server->condition->getIcon())
|
||||
->color(fn (Server $server) => $server->condition->getColor()),
|
||||
->color(fn (Server $server) => $server->condition->getColor())
|
||||
->contextMenuActions($menuOptions)
|
||||
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
|
||||
];
|
||||
|
||||
$viewTwo = [
|
||||
TextColumn::make('name')
|
||||
ContextMenuTextColumn::make('name')
|
||||
->label('')
|
||||
->size('md')
|
||||
->searchable(),
|
||||
TextColumn::make('')
|
||||
->searchable()
|
||||
->contextMenuActions($menuOptions)
|
||||
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
|
||||
ContextMenuTextColumn::make('allocation.address')
|
||||
->label('')
|
||||
->badge()
|
||||
->copyable(request()->isSecure())
|
||||
->copyMessage(fn (Server $server, string $state) => 'Copied ' . $server->allocation->address)
|
||||
->state(fn (Server $server) => $server->allocation->address),
|
||||
->contextMenuActions($menuOptions)
|
||||
->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
|
||||
];
|
||||
|
||||
$viewThree = [
|
||||
@ -190,4 +240,25 @@ class ListServers extends ListRecords
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
#[On('powerAction')]
|
||||
public function powerAction(Server $server, string $action): void
|
||||
{
|
||||
try {
|
||||
$this->daemonPowerRepository->setServer($server)->send($action);
|
||||
|
||||
Notification::make()
|
||||
->title('Power Action')
|
||||
->body($action . ' sent to ' . $server->name)
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$this->redirect(self::getUrl(['activeTab' => $this->activeTab]));
|
||||
} catch (ConnectionException) {
|
||||
Notification::make()
|
||||
->title(trans('exceptions.node.error_connecting', ['node' => $server->node->name]))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class RotateDatabasePasswordAction extends Action
|
||||
|
||||
$this->icon('tabler-refresh');
|
||||
|
||||
$this->authorize(fn (Database $database) => auth()->user()->can('update database', $database));
|
||||
$this->authorize(fn (Database $database) => auth()->user()->can('update', $database));
|
||||
|
||||
$this->modalHeading(trans('admin/databasehost.rotate_password'));
|
||||
|
||||
|
@ -6,5 +6,5 @@ use Filament\Tables\Columns\Column;
|
||||
|
||||
class ServerEntryColumn extends Column
|
||||
{
|
||||
protected string $view = 'tables.columns.server-entry-column';
|
||||
protected string $view = 'livewire.columns.server-entry-column';
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Auth\EditProfile as BaseEditProfile;
|
||||
use Filament\Support\Colors\Color;
|
||||
@ -40,6 +41,7 @@ use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
@ -287,6 +289,8 @@ class EditProfile extends BaseEditProfile
|
||||
);
|
||||
|
||||
Activity::event('user:api-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($token->accessToken)
|
||||
->property('identifier', $token->accessToken->identifier)
|
||||
->log();
|
||||
@ -365,18 +369,8 @@ class EditProfile extends BaseEditProfile
|
||||
Section::make(trans('profile.console'))
|
||||
->collapsible()
|
||||
->icon('tabler-brand-tabler')
|
||||
->columns(4)
|
||||
->schema([
|
||||
TextInput::make('console_rows')
|
||||
->label(trans('profile.rows'))
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->columnSpan(1)
|
||||
->default(30),
|
||||
// Select::make('console_font')
|
||||
// ->label(trans('profile.font'))
|
||||
// ->hidden() //TODO
|
||||
// ->columnSpan(1),
|
||||
TextInput::make('console_font_size')
|
||||
->label(trans('profile.font_size'))
|
||||
->columnSpan(1)
|
||||
@ -384,6 +378,82 @@ class EditProfile extends BaseEditProfile
|
||||
->numeric()
|
||||
->required()
|
||||
->default(14),
|
||||
Select::make('console_font')
|
||||
->label(trans('profile.font'))
|
||||
->required()
|
||||
->options(function () {
|
||||
$fonts = [
|
||||
'monospace' => 'monospace', //default
|
||||
];
|
||||
|
||||
if (!Storage::disk('public')->exists('fonts')) {
|
||||
Storage::disk('public')->makeDirectory('fonts');
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
foreach (Storage::disk('public')->allFiles('fonts') as $file) {
|
||||
$fileInfo = pathinfo($file);
|
||||
|
||||
if ($fileInfo['extension'] === 'ttf') {
|
||||
$fonts[$fileInfo['filename']] = $fileInfo['filename'];
|
||||
}
|
||||
}
|
||||
|
||||
return $fonts;
|
||||
})
|
||||
->reactive()
|
||||
->default('monospace')
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('font_preview', $state)),
|
||||
Placeholder::make('font_preview')
|
||||
->label(trans('profile.font_preview'))
|
||||
->columnSpan(2)
|
||||
->content(function (Get $get) {
|
||||
$fontName = $get('console_font') ?? 'monospace';
|
||||
$fontSize = $get('console_font_size') . 'px';
|
||||
$style = <<<CSS
|
||||
.preview-text {
|
||||
font-family: $fontName;
|
||||
font-size: $fontSize;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
CSS;
|
||||
if ($fontName !== 'monospace') {
|
||||
$fontUrl = asset("storage/fonts/$fontName.ttf");
|
||||
$style = <<<CSS
|
||||
@font-face {
|
||||
font-family: $fontName;
|
||||
src: url("$fontUrl");
|
||||
}
|
||||
$style
|
||||
CSS;
|
||||
}
|
||||
|
||||
return new HtmlString(<<<HTML
|
||||
<style>
|
||||
{$style}
|
||||
</style>
|
||||
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
|
||||
HTML);
|
||||
}),
|
||||
TextInput::make('console_graph_period')
|
||||
->label(trans('profile.graph_period'))
|
||||
->suffix(trans('profile.seconds'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('profile.graph_period_helper'))
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(30)
|
||||
->minValue(10)
|
||||
->maxValue(120)
|
||||
->required(),
|
||||
TextInput::make('console_rows')
|
||||
->label(trans('profile.rows'))
|
||||
->minValue(1)
|
||||
->numeric()
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->default(30),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
@ -446,12 +516,14 @@ class EditProfile extends BaseEditProfile
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$moarbetterdata = [
|
||||
'console_font' => $data['console_font'],
|
||||
'console_font_size' => $data['console_font_size'],
|
||||
'console_rows' => $data['console_rows'],
|
||||
'console_graph_period' => $data['console_graph_period'],
|
||||
'dashboard_layout' => $data['dashboard_layout'],
|
||||
];
|
||||
|
||||
unset($data['dashboard_layout'], $data['console_font_size'], $data['console_rows']);
|
||||
unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout']);
|
||||
$data['customization'] = json_encode($moarbetterdata);
|
||||
|
||||
return $data;
|
||||
@ -461,8 +533,10 @@ class EditProfile extends BaseEditProfile
|
||||
{
|
||||
$moarbetterdata = json_decode($data['customization'], true);
|
||||
|
||||
$data['console_font'] = $moarbetterdata['console_font'] ?? 'monospace';
|
||||
$data['console_font_size'] = $moarbetterdata['console_font_size'] ?? 14;
|
||||
$data['console_rows'] = $moarbetterdata['console_rows'] ?? 30;
|
||||
$data['console_graph_period'] = $moarbetterdata['console_graph_period'] ?? 30;
|
||||
$data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid';
|
||||
|
||||
return $data;
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use App\Events\Auth\ProvidedAuthenticationToken;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\OAuth\Providers\OAuthProvider;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions;
|
||||
@ -54,14 +56,37 @@ class Login extends BaseLogin
|
||||
if ($token === null) {
|
||||
$this->verifyTwoFactor = true;
|
||||
|
||||
Activity::event('auth:checkpoint')
|
||||
->withRequestMetadata()
|
||||
->subject($user)
|
||||
->log();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$isValidToken = $this->google2FA->verifyKey(
|
||||
$user->totp_secret,
|
||||
$token,
|
||||
Config::integer('panel.auth.2fa.window'),
|
||||
);
|
||||
$isValidToken = false;
|
||||
if (strlen($token) === $this->google2FA->getOneTimePasswordLength()) {
|
||||
$isValidToken = $this->google2FA->verifyKey(
|
||||
$user->totp_secret,
|
||||
$token,
|
||||
Config::integer('panel.auth.2fa.window'),
|
||||
);
|
||||
|
||||
if ($isValidToken) {
|
||||
event(new ProvidedAuthenticationToken($user));
|
||||
}
|
||||
} else {
|
||||
foreach ($user->recoveryTokens as $recoveryToken) {
|
||||
if (password_verify($token, $recoveryToken->token)) {
|
||||
$isValidToken = true;
|
||||
$recoveryToken->delete();
|
||||
|
||||
event(new ProvidedAuthenticationToken($user, true));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isValidToken) {
|
||||
// Buffer to prevent bruteforce
|
||||
@ -108,7 +133,9 @@ class Login extends BaseLogin
|
||||
{
|
||||
return TextInput::make('2fa')
|
||||
->label(trans('auth.two-factor-code'))
|
||||
->hidden(fn () => !$this->verifyTwoFactor)
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('auth.two-factor-hint'))
|
||||
->visible(fn () => $this->verifyTwoFactor)
|
||||
->required()
|
||||
->live();
|
||||
}
|
||||
|
@ -2,43 +2,27 @@
|
||||
|
||||
namespace App\Filament\Server\Components;
|
||||
|
||||
use Closure;
|
||||
use Filament\Support\Concerns\EvaluatesClosures;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class SmallStatBlock extends Stat
|
||||
{
|
||||
protected string|Htmlable $label;
|
||||
use EvaluatesClosures;
|
||||
|
||||
protected $value;
|
||||
protected bool|Closure $copyOnClick = false;
|
||||
|
||||
public function label(string|Htmlable $label): static
|
||||
public function copyOnClick(bool|Closure $copyOnClick = true): static
|
||||
{
|
||||
$this->label = $label;
|
||||
$this->copyOnClick = $copyOnClick;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function value($value): static
|
||||
public function shouldCopyOnClick(): bool
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): string|Htmlable
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return value($this->value);
|
||||
}
|
||||
|
||||
public function toHtml(): string
|
||||
{
|
||||
return $this->render()->render();
|
||||
return $this->evaluate($this->copyOnClick);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Components;
|
||||
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class StatBlock extends Stat
|
||||
{
|
||||
protected string|Htmlable $label;
|
||||
|
||||
protected $value;
|
||||
|
||||
public function label(string|Htmlable $label): static
|
||||
{
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function value($value): static
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLabel(): string|Htmlable
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getValue()
|
||||
{
|
||||
return value($this->value);
|
||||
}
|
||||
|
||||
public function toHtml(): string
|
||||
{
|
||||
return $this->render()->render();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('filament.components.server-data-block', $this->data());
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ use App\Extensions\Features\FeatureProvider;
|
||||
use App\Filament\Server\Widgets\ServerConsole;
|
||||
use App\Filament\Server\Widgets\ServerCpuChart;
|
||||
use App\Filament\Server\Widgets\ServerMemoryChart;
|
||||
// use App\Filament\Server\Widgets\ServerNetworkChart;
|
||||
use App\Filament\Server\Widgets\ServerNetworkChart;
|
||||
use App\Filament\Server\Widgets\ServerOverview;
|
||||
use App\Livewire\AlertBanner;
|
||||
use App\Models\Permission;
|
||||
@ -112,7 +112,7 @@ class Console extends Page
|
||||
$allWidgets = array_merge($allWidgets, [
|
||||
ServerCpuChart::class,
|
||||
ServerMemoryChart::class,
|
||||
//ServerNetworkChart::class, TODO: convert units.
|
||||
ServerNetworkChart::class,
|
||||
]);
|
||||
|
||||
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Bottom->value] ?? []);
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\ActivityResource\Pages;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Permission;
|
||||
@ -9,9 +11,20 @@ use App\Models\Role;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class ActivityResource extends Resource
|
||||
{
|
||||
@ -25,12 +38,101 @@ class ActivityResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-stack';
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated([25, 50])
|
||||
->defaultPaginationPageOption(25)
|
||||
->columns([
|
||||
TextColumn::make('event')
|
||||
->html()
|
||||
->description(fn ($state) => $state)
|
||||
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
|
||||
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
|
||||
TextColumn::make('user')
|
||||
->state(function (ActivityLog $activityLog) use ($server) {
|
||||
if (!$activityLog->actor instanceof User) {
|
||||
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
|
||||
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
|
||||
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
|
||||
$user .= " ({$activityLog->actor->email})";
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
|
||||
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor) ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
|
||||
->grow(false),
|
||||
DateTimeColumn::make('timestamp')
|
||||
->since()
|
||||
->sortable()
|
||||
->grow(false),
|
||||
])
|
||||
->defaultSort('timestamp', 'desc')
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
|
||||
->form([
|
||||
Placeholder::make('event')
|
||||
->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
|
||||
TextInput::make('user')
|
||||
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
|
||||
if (!$activityLog->actor instanceof User) {
|
||||
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
|
||||
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
|
||||
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
|
||||
$user .= " ({$activityLog->actor->email})";
|
||||
}
|
||||
|
||||
if (auth()->user()->can('seeIps activityLog')) {
|
||||
$user .= " - $activityLog->ip";
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->hintAction(
|
||||
Action::make('edit')
|
||||
->label(trans('filament-actions::edit.single.label'))
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor))
|
||||
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
|
||||
),
|
||||
DateTimePicker::make('timestamp'),
|
||||
KeyValue::make('properties')
|
||||
->label('Metadata')
|
||||
->formatStateUsing(fn ($state) => Arr::dot($state)),
|
||||
]),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('event')
|
||||
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
|
||||
->searchable()
|
||||
->preload(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id))
|
||||
return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id)->where('subject_type', $server->getMorphClass()))
|
||||
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
|
||||
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
|
||||
// We could do this with a query and a lot of joins, but that gets pretty
|
||||
@ -51,11 +153,6 @@ class ActivityResource extends Resource
|
||||
});
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
@ -2,114 +2,13 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\ActivityResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
|
||||
use App\Filament\Server\Resources\ActivityResource;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class ListActivities extends ListRecords
|
||||
{
|
||||
protected static string $resource = ActivityResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated([25, 50])
|
||||
->defaultPaginationPageOption(25)
|
||||
->columns([
|
||||
TextColumn::make('event')
|
||||
->html()
|
||||
->description(fn ($state) => $state)
|
||||
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
|
||||
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
|
||||
TextColumn::make('user')
|
||||
->state(function (ActivityLog $activityLog) use ($server) {
|
||||
if (!$activityLog->actor instanceof User) {
|
||||
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
|
||||
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
|
||||
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
|
||||
$user .= " ({$activityLog->actor->email})";
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
|
||||
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user') ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
|
||||
->grow(false),
|
||||
DateTimeColumn::make('timestamp')
|
||||
->since()
|
||||
->sortable()
|
||||
->grow(false),
|
||||
])
|
||||
->defaultSort('timestamp', 'desc')
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
|
||||
->form([
|
||||
Placeholder::make('event')
|
||||
->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
|
||||
TextInput::make('user')
|
||||
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
|
||||
if (!$activityLog->actor instanceof User) {
|
||||
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
|
||||
// Only show the email if the actor is the server owner/ a subuser or if the viewing user is an admin
|
||||
if (auth()->user()->isAdmin() || $server->owner_id === $activityLog->actor->id || $server->subusers->where('user_id', $activityLog->actor->id)->first()) {
|
||||
$user .= " ({$activityLog->actor->email})";
|
||||
}
|
||||
|
||||
if (auth()->user()->can('seeIps activityLog')) {
|
||||
$user .= " - $activityLog->ip";
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
->hintAction(
|
||||
Action::make('edit')
|
||||
->label(trans('filament-actions::edit.single.label'))
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update user'))
|
||||
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
|
||||
),
|
||||
DateTimePicker::make('timestamp'),
|
||||
KeyValue::make('properties')
|
||||
->label('Metadata')
|
||||
->formatStateUsing(fn ($state) => Arr::dot($state)),
|
||||
]),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('event')
|
||||
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
|
||||
->searchable()
|
||||
->preload(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
|
@ -2,12 +2,18 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\AllocationResource\Pages;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\DetachAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AllocationResource extends Resource
|
||||
@ -22,6 +28,61 @@ class AllocationResource extends Resource
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-network';
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('ip')
|
||||
->label('Address')
|
||||
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
|
||||
TextColumn::make('alias')
|
||||
->hidden(),
|
||||
TextColumn::make('port'),
|
||||
TextInputColumn::make('notes')
|
||||
->visibleFrom('sm')
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
|
||||
->label('Notes')
|
||||
->placeholder('No Notes'),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(function (Allocation $allocation) use ($server) {
|
||||
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
|
||||
return $server->update(['allocation_id' => $allocation->id]);
|
||||
}
|
||||
})
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->label('Primary'),
|
||||
])
|
||||
->actions([
|
||||
DetachAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
|
||||
->label('Delete')
|
||||
->icon('tabler-trash')
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->action(function (Allocation $allocation) {
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
|
@ -4,85 +4,24 @@ namespace App\Filament\Server\Resources\AllocationResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\AllocationResource;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\FindAssignableAllocationService;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DetachAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListAllocations extends ListRecords
|
||||
{
|
||||
protected static string $resource = AllocationResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('ip')
|
||||
->label('Address')
|
||||
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
|
||||
TextColumn::make('alias')
|
||||
->hidden(),
|
||||
TextColumn::make('port'),
|
||||
TextInputColumn::make('notes')
|
||||
->visibleFrom('sm')
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
|
||||
->label('Notes')
|
||||
->placeholder('No Notes'),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(function (Allocation $allocation) use ($server) {
|
||||
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
|
||||
return $server->update(['allocation_id' => $allocation->id]);
|
||||
}
|
||||
})
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->label('Primary'),
|
||||
])
|
||||
->actions([
|
||||
DetachAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
|
||||
->label('Delete')
|
||||
->icon('tabler-trash')
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->action(function (Allocation $allocation) {
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Actions\Action::make('addAllocation')
|
||||
Action::make('addAllocation')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
|
||||
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
|
||||
->hidden(fn () => !config('panel.client_features.allocations.enabled'))
|
||||
@ -93,7 +32,7 @@ class ListAllocations extends ListRecords
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
}),
|
||||
];
|
||||
|
@ -2,13 +2,37 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Enums\BackupStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\BackupResource\Pages;
|
||||
use App\Http\Controllers\Api\Client\Servers\BackupController;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonBackupRepository;
|
||||
use App\Services\Backups\DownloadLinkService;
|
||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Services\Backups\DeleteBackupService;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BackupResource extends Resource
|
||||
{
|
||||
@ -44,8 +68,138 @@ class BackupResource extends Resource
|
||||
return null;
|
||||
}
|
||||
|
||||
return $count >= $limit ? 'danger'
|
||||
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->columnSpanFull(),
|
||||
TextArea::make('ignored')
|
||||
->columnSpanFull()
|
||||
->label('Ignored Files & Directories'),
|
||||
Toggle::make('is_locked')
|
||||
->label('Lock?')
|
||||
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
BytesColumn::make('bytes')
|
||||
->label('Size'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label('Created')
|
||||
->since()
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge(),
|
||||
IconColumn::make('is_locked')
|
||||
->visibleFrom('md')
|
||||
->label('Lock Status')
|
||||
->trueIcon('tabler-lock')
|
||||
->falseIcon('tabler-lock-open'),
|
||||
])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
Action::make('lock')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
|
||||
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('download')
|
||||
->color('primary')
|
||||
->icon('tabler-download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
|
||||
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('restore')
|
||||
->color('success')
|
||||
->icon('tabler-folder-up')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
|
||||
->form([
|
||||
Placeholder::make('')
|
||||
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
|
||||
Checkbox::make('truncate')
|
||||
->label('Delete all files before restoring backup?'),
|
||||
])
|
||||
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
|
||||
if (!is_null($server->status)) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This server is not currently in a state that allows for a backup to be restored.')
|
||||
->send();
|
||||
}
|
||||
|
||||
if (!$backup->is_successful && is_null($backup->completed_at)) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This backup cannot be restored at this time: not completed or failed.')
|
||||
->send();
|
||||
}
|
||||
|
||||
$log = Activity::event('server:backup.restore')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
|
||||
|
||||
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
|
||||
// If the backup is for an S3 file we need to generate a unique Download link for
|
||||
// it that will allow daemon to actually access the file.
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$url = $downloadLinkService->handle($backup, auth()->user());
|
||||
}
|
||||
|
||||
// Update the status right away for the server so that we know not to allow certain
|
||||
// actions against it via the Panel API.
|
||||
$server->update(['status' => ServerState::RestoringBackup]);
|
||||
|
||||
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
|
||||
});
|
||||
|
||||
return Notification::make()
|
||||
->title('Restoring Backup')
|
||||
->send();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
DeleteAction::make('delete')
|
||||
->disabled(fn (Backup $backup) => $backup->is_locked)
|
||||
->modalDescription(fn (Backup $backup) => 'Do you wish to delete ' . $backup->name . '?')
|
||||
->modalSubmitActionLabel('Delete Backup')
|
||||
->action(function (Backup $backup, DeleteBackupService $deleteBackupService) {
|
||||
try {
|
||||
$deleteBackupService->handle($backup);
|
||||
} catch (ConnectionException) {
|
||||
Notification::make()
|
||||
->title('Could not delete backup')
|
||||
->body('Connection to node failed')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Activity::event('server:backup.delete')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'failed' => !$backup->is_successful])
|
||||
->log();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
|
@ -2,165 +2,28 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\BackupResource\Pages;
|
||||
|
||||
use App\Enums\BackupStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\BackupResource;
|
||||
use App\Http\Controllers\Api\Client\Servers\BackupController;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonBackupRepository;
|
||||
use App\Services\Backups\DownloadLinkService;
|
||||
use App\Services\Backups\InitiateBackupService;
|
||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class ListBackups extends ListRecords
|
||||
{
|
||||
protected static string $resource = BackupResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->columnSpanFull(),
|
||||
TextArea::make('ignored')
|
||||
->columnSpanFull()
|
||||
->label('Ignored Files & Directories'),
|
||||
Toggle::make('is_locked')
|
||||
->label('Lock?')
|
||||
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
BytesColumn::make('bytes')
|
||||
->label('Size'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label('Created')
|
||||
->since()
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge(),
|
||||
IconColumn::make('is_locked')
|
||||
->visibleFrom('md')
|
||||
->label('Lock Status')
|
||||
->trueIcon('tabler-lock')
|
||||
->falseIcon('tabler-lock-open'),
|
||||
])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
Action::make('lock')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
|
||||
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('download')
|
||||
->color('primary')
|
||||
->icon('tabler-download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
|
||||
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('restore')
|
||||
->color('success')
|
||||
->icon('tabler-folder-up')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
|
||||
->form([
|
||||
Placeholder::make('')
|
||||
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
|
||||
Checkbox::make('truncate')
|
||||
->label('Delete all files before restoring backup?'),
|
||||
])
|
||||
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
|
||||
if (!is_null($server->status)) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This server is not currently in a state that allows for a backup to be restored.')
|
||||
->send();
|
||||
}
|
||||
|
||||
if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This backup cannot be restored at this time: not completed or failed.')
|
||||
->send();
|
||||
}
|
||||
|
||||
$log = Activity::event('server:backup.restore')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
|
||||
|
||||
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
|
||||
// If the backup is for an S3 file we need to generate a unique Download link for
|
||||
// it that will allow daemon to actually access the file.
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$url = $downloadLinkService->handle($backup, auth()->user());
|
||||
}
|
||||
|
||||
// Update the status right away for the server so that we know not to allow certain
|
||||
// actions against it via the Panel API.
|
||||
$server->update(['status' => ServerState::RestoringBackup]);
|
||||
|
||||
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
|
||||
});
|
||||
|
||||
return Notification::make()
|
||||
->title('Restoring Backup')
|
||||
->send();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
DeleteAction::make('delete')
|
||||
->disabled(fn (Backup $backup) => $backup->is_locked)
|
||||
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
|
||||
->modalSubmitActionLabel('Delete Backup')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup))
|
||||
->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
CreateAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
|
||||
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
|
||||
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
|
||||
|
@ -2,13 +2,23 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
use App\Models\Database;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class DatabaseResource extends Resource
|
||||
{
|
||||
@ -42,9 +52,65 @@ class DatabaseResource extends Resource
|
||||
return null;
|
||||
}
|
||||
|
||||
return $count >= $limit
|
||||
? 'danger'
|
||||
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
return $count >= $limit ? 'danger' : ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->formatStateUsing(fn (Database $database) => $database->address())
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('database')
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('username')
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('password')
|
||||
->password()->revealable()
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->hintAction(
|
||||
RotateDatabasePasswordAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
|
||||
)
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')
|
||||
->label('Connections From'),
|
||||
TextInput::make('max_connections')
|
||||
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
|
||||
TextInput::make('jdbc')
|
||||
->label('JDBC Connection String')
|
||||
->password()->revealable()
|
||||
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn (Database $database) => $database->jdbc),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('host')
|
||||
->state(fn (Database $database) => $database->address())
|
||||
->badge(),
|
||||
TextColumn::make('database'),
|
||||
TextColumn::make('username'),
|
||||
TextColumn::make('remote'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||
DeleteAction::make()
|
||||
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
|
||||
]);
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
|
@ -2,13 +2,8 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\DatabaseResource;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use Filament\Actions\CreateAction;
|
||||
@ -16,81 +11,12 @@ use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class ListDatabases extends ListRecords
|
||||
{
|
||||
protected static string $resource = DatabaseResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->formatStateUsing(fn (Database $database) => $database->address())
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('database')
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('username')
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
|
||||
TextInput::make('password')
|
||||
->password()->revealable()
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->hintAction(
|
||||
RotateDatabasePasswordAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
|
||||
)
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')
|
||||
->label('Connections From'),
|
||||
TextInput::make('max_connections')
|
||||
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
|
||||
TextInput::make('jdbc')
|
||||
->label('JDBC Connection String')
|
||||
->password()->revealable()
|
||||
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn (Database $database) => $database->jdbc),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('host')
|
||||
->state(fn (Database $database) => $database->address())
|
||||
->badge(),
|
||||
TextColumn::make('database'),
|
||||
TextColumn::make('username'),
|
||||
TextColumn::make('remote'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||
DeleteAction::make()
|
||||
->after(function (Database $database) {
|
||||
Activity::event('server:database.delete')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
|
@ -26,6 +26,8 @@ use Filament\Resources\Pages\Page;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Livewire\Attributes\Locked;
|
||||
@ -128,31 +130,33 @@ class EditFiles extends Page
|
||||
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
|
||||
} catch (FileSizeTooLargeException) {
|
||||
AlertBanner::make()
|
||||
->title('File too large!')
|
||||
->body('<code>' . $this->path . '</code> Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
|
||||
->title('<code>' . basename($this->path) . '</code> is too large!')
|
||||
->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl());
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
} catch (FileNotFoundException) {
|
||||
AlertBanner::make()
|
||||
->title('File Not found!')
|
||||
->body('<code>' . $this->path . '</code>')
|
||||
->title('<code>' . basename($this->path) . '</code> not found!')
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl());
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
} catch (FileNotEditableException) {
|
||||
AlertBanner::make()
|
||||
->title('Could not edit directory!')
|
||||
->body('<code>' . $this->path . '</code>')
|
||||
->title('<code>' . basename($this->path) . '</code> is a directory')
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl());
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
} catch (ConnectionException) {
|
||||
// Alert banner for this one will be handled by ListFiles
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
}
|
||||
})
|
||||
->language(fn (Get $get) => $get('lang'))
|
||||
@ -176,6 +180,15 @@ class EditFiles extends Page
|
||||
->info()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
try {
|
||||
$this->getDaemonFileRepository()->getDirectory('/');
|
||||
} catch (ConnectionException) {
|
||||
AlertBanner::make('node_connection_error')
|
||||
->title('Could not connect to the node!')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,6 +243,14 @@ class EditFiles extends Page
|
||||
return $this->fileRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
|
||||
{
|
||||
return parent::getUrl($parameters, $isAbsolute, $panel, $tenant) . '/';
|
||||
}
|
||||
|
||||
public static function route(string $path): PageRegistration
|
||||
{
|
||||
return new PageRegistration(
|
||||
|
@ -12,7 +12,6 @@ use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Livewire\AlertBanner;
|
||||
use Filament\Actions\Action as HeaderAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
@ -30,14 +29,12 @@ use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Carbon;
|
||||
@ -49,30 +46,10 @@ class ListFiles extends ListRecords
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
public string $path = '/';
|
||||
|
||||
private DaemonFileRepository $fileRepository;
|
||||
|
||||
private bool $isDisabled = false;
|
||||
|
||||
public function mount(?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
|
||||
$this->path = $path ?? '/';
|
||||
|
||||
try {
|
||||
$this->getDaemonFileRepository()->getDirectory('/');
|
||||
} catch (ConnectionException) {
|
||||
$this->isDisabled = true;
|
||||
|
||||
AlertBanner::make('node_connection_error')
|
||||
->title('Could not connect to the node!')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
$resource = static::getResource();
|
||||
@ -130,21 +107,18 @@ class ListFiles extends ListRecords
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Open')
|
||||
->icon('tabler-eye')
|
||||
->visible(fn (File $file) => $file->is_directory)
|
||||
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
EditAction::make('edit')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (File $file) => $file->canEdit())
|
||||
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
ActionGroup::make([
|
||||
Action::make('rename')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Rename')
|
||||
->icon('tabler-forms')
|
||||
->form([
|
||||
@ -173,7 +147,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('copy')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Copy')
|
||||
->icon('tabler-copy')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
@ -193,14 +166,12 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Download')
|
||||
->icon('tabler-download')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
|
||||
Action::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Move')
|
||||
->icon('tabler-replace')
|
||||
->form([
|
||||
@ -236,7 +207,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('permissions')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Permissions')
|
||||
->icon('tabler-license')
|
||||
->form([
|
||||
@ -293,7 +263,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Archive')
|
||||
->icon('tabler-archive')
|
||||
->form([
|
||||
@ -321,7 +290,6 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('unarchive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Unarchive')
|
||||
->icon('tabler-archive')
|
||||
->visible(fn (File $file) => $file->isArchive())
|
||||
@ -343,13 +311,12 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
DeleteAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->modalDescription(fn (File $file) => $file->name)
|
||||
->modalHeading('Delete file?')
|
||||
->modalHeading(fn (File $file) => trans('filament-actions::delete.single.modal.heading', ['label' => $file->name . ' ' . ($file->is_directory ? 'folder' : 'file')]))
|
||||
->action(function (File $file) {
|
||||
$this->deselectAllTableRecords();
|
||||
$this->getDaemonFileRepository()->deleteFiles($this->path, [$file->name]);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
@ -358,83 +325,77 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
BulkAction::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('Directory')
|
||||
->hint('Enter the new directory, relative to the current directory.')
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
||||
])
|
||||
->action(function (Collection $files, $data) {
|
||||
$location = rtrim($data['location'], '/');
|
||||
->groupedBulkActions([
|
||||
BulkAction::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('Directory')
|
||||
->hint('Enter the new directory, relative to the current directory.')
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
||||
])
|
||||
->action(function (Collection $files, $data) {
|
||||
$location = rtrim($data['location'], '/');
|
||||
|
||||
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
|
||||
$this->getDaemonFileRepository()
|
||||
->renameFiles($this->path, $files);
|
||||
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
|
||||
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
BulkAction::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Archive name')
|
||||
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
||||
->suffix('.tar.gz'),
|
||||
])
|
||||
->action(function ($data, Collection $files) {
|
||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
BulkAction::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Archive name')
|
||||
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
||||
->suffix('.tar.gz'),
|
||||
])
|
||||
->action(function ($data, Collection $files) {
|
||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||
|
||||
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
|
||||
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
|
||||
|
||||
Activity::event('server:file.compress')
|
||||
->property('name', $archive['name'])
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
Activity::event('server:file.compress')
|
||||
->property('name', $archive['name'])
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->body($archive['name'])
|
||||
->success()
|
||||
->send();
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->body($archive['name'])
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->action(function (Collection $files) {
|
||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->action(function (Collection $files) {
|
||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files deleted.')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
]),
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files deleted.')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -446,7 +407,6 @@ class ListFiles extends ListRecords
|
||||
return [
|
||||
HeaderAction::make('new_file')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('New File')
|
||||
->color('gray')
|
||||
->keyBindings('')
|
||||
@ -478,7 +438,6 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
HeaderAction::make('new_folder')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('New Folder')
|
||||
->color('gray')
|
||||
->action(function ($data) {
|
||||
@ -495,7 +454,6 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
HeaderAction::make('upload')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Upload')
|
||||
->action(function ($data) {
|
||||
if (count($data['files']) > 0 && !isset($data['url'])) {
|
||||
@ -545,14 +503,14 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
HeaderAction::make('search')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->disabled($this->isDisabled)
|
||||
->label('Global Search')
|
||||
->modalSubmitActionLabel('Search')
|
||||
->form([
|
||||
TextInput::make('searchTerm')
|
||||
->placeholder('Enter a search term, e.g. *.txt')
|
||||
->required()
|
||||
->regex('/^[^*]*\*?[^*]*$/')
|
||||
->minLength(3),
|
||||
->minValue(3),
|
||||
])
|
||||
->action(fn ($data) => redirect(SearchFiles::getUrl([
|
||||
'searchTerm' => $data['searchTerm'],
|
||||
|
@ -12,6 +12,7 @@ use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class SearchFiles extends ListRecords
|
||||
{
|
||||
@ -22,15 +23,8 @@ class SearchFiles extends ListRecords
|
||||
#[Locked]
|
||||
public string $searchTerm;
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public function mount(?string $searchTerm = null, ?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->path = $path ?? '/';
|
||||
}
|
||||
#[Url]
|
||||
public string $path = '/';
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
|
||||
use App\Helpers\Utilities;
|
||||
@ -23,6 +25,12 @@ use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScheduleResource extends Resource
|
||||
@ -303,6 +311,44 @@ class ScheduleResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('cron')
|
||||
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
|
||||
TextColumn::make('status')
|
||||
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
|
||||
IconColumn::make('only_when_online')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('last_run_at')
|
||||
->label('Last run')
|
||||
->placeholder('Never')
|
||||
->since()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('next_run_at')
|
||||
->label('Next run')
|
||||
->placeholder('Never')
|
||||
->since()
|
||||
->sortable()
|
||||
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->after(function (Schedule $schedule) {
|
||||
Activity::event('server:schedule.delete')
|
||||
->subject($schedule)
|
||||
->property('name', $schedule->name)
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
@ -2,65 +2,19 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Models\Schedule;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListSchedules extends ListRecords
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('cron')
|
||||
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
|
||||
TextColumn::make('status')
|
||||
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
|
||||
IconColumn::make('only_when_online')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('last_run_at')
|
||||
->label('Last run')
|
||||
->placeholder('Never')
|
||||
->since()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('next_run_at')
|
||||
->label('Next run')
|
||||
->placeholder('Never')
|
||||
->since()
|
||||
->sortable()
|
||||
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make()
|
||||
->after(function (Schedule $schedule) {
|
||||
Activity::event('server:schedule.delete')
|
||||
->subject($schedule)
|
||||
->property('name', $schedule->name)
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()->label('New Schedule'),
|
||||
CreateAction::make()
|
||||
->label('New Schedule'),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,8 @@ use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Schedule;
|
||||
use App\Services\Schedules\ProcessScheduleService;
|
||||
use Filament\Actions;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
@ -18,7 +19,7 @@ class ViewSchedule extends ViewRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\Action::make('runNow')
|
||||
Action::make('runNow')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant()))
|
||||
->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now'))
|
||||
->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary')
|
||||
@ -33,7 +34,7 @@ class ViewSchedule extends ViewRecord
|
||||
|
||||
$this->fillForm();
|
||||
}),
|
||||
Actions\EditAction::make(),
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,8 @@ use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
@ -83,6 +83,35 @@ class UserResource extends Resource
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$tabs = [];
|
||||
$permissionsArray = [];
|
||||
|
||||
foreach (Permission::permissionData() as $data) {
|
||||
$options = [];
|
||||
$descriptions = [];
|
||||
|
||||
foreach ($data['permissions'] as $permission) {
|
||||
$options[$permission] = str($permission)->headline();
|
||||
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
$permissionsArray[$data['name']][] = $permission;
|
||||
}
|
||||
|
||||
$tabs[] = Tab::make(str($data['name'])->headline())
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
|
||||
->icon($data['icon'])
|
||||
->schema([
|
||||
CheckboxList::make($data['name'])
|
||||
->label('')
|
||||
->bulkToggleable()
|
||||
->columns(2)
|
||||
->options($options)
|
||||
->descriptions($descriptions),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
return $table
|
||||
->paginated(false)
|
||||
->searchable(false)
|
||||
@ -158,69 +187,8 @@ class UserResource extends Resource
|
||||
Actions::make([
|
||||
Action::make('assignAll')
|
||||
->label('Assign All')
|
||||
->action(function (Set $set) {
|
||||
$permissions = [
|
||||
'control' => [
|
||||
'console',
|
||||
'start',
|
||||
'stop',
|
||||
'restart',
|
||||
],
|
||||
'user' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'file' => [
|
||||
'read',
|
||||
'read-content',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'archive',
|
||||
'sftp',
|
||||
],
|
||||
'backup' => [
|
||||
'read',
|
||||
'create',
|
||||
'delete',
|
||||
'download',
|
||||
'restore',
|
||||
],
|
||||
'allocation' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'startup' => [
|
||||
'read',
|
||||
'update',
|
||||
'docker-image',
|
||||
],
|
||||
'database' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'view_password',
|
||||
],
|
||||
'schedule' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'settings' => [
|
||||
'rename',
|
||||
'reinstall',
|
||||
],
|
||||
'activity' => [
|
||||
'read',
|
||||
],
|
||||
];
|
||||
|
||||
->action(function (Set $set) use ($permissionsArray) {
|
||||
$permissions = $permissionsArray;
|
||||
foreach ($permissions as $key => $value) {
|
||||
$allValues = array_unique($value);
|
||||
$set($key, $allValues);
|
||||
@ -235,264 +203,25 @@ class UserResource extends Resource
|
||||
]),
|
||||
Tabs::make()
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Tab::make('Console')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.control_desc'))
|
||||
->icon('tabler-terminal-2')
|
||||
->schema([
|
||||
CheckboxList::make('control')
|
||||
->formatStateUsing(function (User $user, Set $set) use ($server) {
|
||||
$permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions;
|
||||
|
||||
$transformedPermissions = [];
|
||||
|
||||
foreach ($permissionsArray as $permission) {
|
||||
[$group, $action] = explode('.', $permission, 2);
|
||||
$transformedPermissions[$group][] = $action;
|
||||
}
|
||||
|
||||
foreach ($transformedPermissions as $key => $value) {
|
||||
$set($key, $value);
|
||||
}
|
||||
|
||||
return $transformedPermissions['control'] ?? [];
|
||||
})
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'console' => 'Console',
|
||||
'start' => 'Start',
|
||||
'stop' => 'Stop',
|
||||
'restart' => 'Restart',
|
||||
])
|
||||
->descriptions([
|
||||
'console' => trans('server/users.permissions.control_console'),
|
||||
'start' => trans('server/users.permissions.control_start'),
|
||||
'stop' => trans('server/users.permissions.control_stop'),
|
||||
'restart' => trans('server/users.permissions.control_restart'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('User')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.user_desc'))
|
||||
->icon('tabler-users')
|
||||
->schema([
|
||||
CheckboxList::make('user')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.user_create'),
|
||||
'read' => trans('server/users.permissions.user_read'),
|
||||
'update' => trans('server/users.permissions.user_update'),
|
||||
'delete' => trans('server/users.permissions.user_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('File')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.file_desc'))
|
||||
->icon('tabler-folders')
|
||||
->schema([
|
||||
CheckboxList::make('file')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'read-content' => 'Read Content',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'archive' => 'Archive',
|
||||
'sftp' => 'SFTP',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.file_create'),
|
||||
'read' => trans('server/users.permissions.file_read'),
|
||||
'read-content' => trans('server/users.permissions.file_read_content'),
|
||||
'update' => trans('server/users.permissions.file_update'),
|
||||
'delete' => trans('server/users.permissions.file_delete'),
|
||||
'archive' => trans('server/users.permissions.file_archive'),
|
||||
'sftp' => trans('server/users.permissions.file_sftp'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Backup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.backup_desc'))
|
||||
->icon('tabler-download')
|
||||
->schema([
|
||||
CheckboxList::make('backup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'delete' => 'Delete',
|
||||
'download' => 'Download',
|
||||
'restore' => 'Restore',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.backup_create'),
|
||||
'read' => trans('server/users.permissions.backup_read'),
|
||||
'delete' => trans('server/users.permissions.backup_delete'),
|
||||
'download' => trans('server/users.permissions.backup_download'),
|
||||
'restore' => trans('server/users.permissions.backup_restore'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Allocation')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.allocation_desc'))
|
||||
->icon('tabler-network')
|
||||
->schema([
|
||||
CheckboxList::make('allocation')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.allocation_read'),
|
||||
'create' => trans('server/users.permissions.allocation_create'),
|
||||
'update' => trans('server/users.permissions.allocation_update'),
|
||||
'delete' => trans('server/users.permissions.allocation_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Startup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.startup_desc'))
|
||||
->icon('tabler-question-mark')
|
||||
->schema([
|
||||
CheckboxList::make('startup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'update' => 'Update',
|
||||
'docker-image' => 'Docker Image',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.startup_read'),
|
||||
'update' => trans('server/users.permissions.startup_update'),
|
||||
'docker-image' => trans('server/users.permissions.startup_docker_image'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Database')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.database_desc'))
|
||||
->icon('tabler-database')
|
||||
->schema([
|
||||
CheckboxList::make('database')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'view_password' => 'View Password',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.database_read'),
|
||||
'create' => trans('server/users.permissions.database_create'),
|
||||
'update' => trans('server/users.permissions.database_update'),
|
||||
'delete' => trans('server/users.permissions.database_delete'),
|
||||
'view_password' => trans('server/users.permissions.database_view_password'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Schedule')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.schedule_desc'))
|
||||
->icon('tabler-clock')
|
||||
->schema([
|
||||
CheckboxList::make('schedule')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.schedule_read'),
|
||||
'create' => trans('server/users.permissions.schedule_create'),
|
||||
'update' => trans('server/users.permissions.schedule_update'),
|
||||
'delete' => trans('server/users.permissions.schedule_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Settings')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.settings_desc'))
|
||||
->icon('tabler-settings')
|
||||
->schema([
|
||||
CheckboxList::make('settings')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'rename' => 'Rename',
|
||||
'reinstall' => 'Reinstall',
|
||||
])
|
||||
->descriptions([
|
||||
'rename' => trans('server/users.permissions.setting_rename'),
|
||||
'reinstall' => trans('server/users.permissions.setting_reinstall'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Activity')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.activity_desc'))
|
||||
->icon('tabler-stack')
|
||||
->schema([
|
||||
CheckboxList::make('activity')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.activity_read'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
->schema($tabs),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->mutateRecordDataUsing(function ($data, User $user) use ($server) {
|
||||
$permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions;
|
||||
|
||||
$transformedPermissions = [];
|
||||
|
||||
foreach ($permissionsArray as $permission) {
|
||||
[$group, $action] = explode('.', $permission, 2);
|
||||
$transformedPermissions[$group][] = $action;
|
||||
}
|
||||
|
||||
foreach ($transformedPermissions as $key => $value) {
|
||||
$data[$key] = $value;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,8 @@ use App\Services\Subusers\SubuserCreationService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions as assignAll;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Actions as assignAll;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Section;
|
||||
@ -32,6 +32,35 @@ class ListUsers extends ListRecords
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$tabs = [];
|
||||
$permissionsArray = [];
|
||||
|
||||
foreach (Permission::permissionData() as $data) {
|
||||
$options = [];
|
||||
$descriptions = [];
|
||||
|
||||
foreach ($data['permissions'] as $permission) {
|
||||
$options[$permission] = str($permission)->headline();
|
||||
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
$permissionsArray[$data['name']][] = $permission;
|
||||
}
|
||||
|
||||
$tabs[] = Tab::make(str($data['name'])->headline())
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
|
||||
->icon($data['icon'])
|
||||
->schema([
|
||||
CheckboxList::make($data['name'])
|
||||
->label('')
|
||||
->bulkToggleable()
|
||||
->columns(2)
|
||||
->options($options)
|
||||
->descriptions($descriptions),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
Actions\CreateAction::make('invite')
|
||||
->label('Invite User')
|
||||
@ -60,72 +89,10 @@ class ListUsers extends ListRecords
|
||||
assignAll::make([
|
||||
Action::make('assignAll')
|
||||
->label('Assign All')
|
||||
->action(function (Set $set, Get $get) {
|
||||
$permissions = [
|
||||
'control' => [
|
||||
'console',
|
||||
'start',
|
||||
'stop',
|
||||
'restart',
|
||||
],
|
||||
'user' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'file' => [
|
||||
'read',
|
||||
'read-content',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'archive',
|
||||
'sftp',
|
||||
],
|
||||
'backup' => [
|
||||
'read',
|
||||
'create',
|
||||
'delete',
|
||||
'download',
|
||||
'restore',
|
||||
],
|
||||
'allocation' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'startup' => [
|
||||
'read',
|
||||
'update',
|
||||
'docker-image',
|
||||
],
|
||||
'database' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'view_password',
|
||||
],
|
||||
'schedule' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'settings' => [
|
||||
'rename',
|
||||
'reinstall',
|
||||
],
|
||||
'activity' => [
|
||||
'read',
|
||||
],
|
||||
];
|
||||
|
||||
->action(function (Set $set, Get $get) use ($permissionsArray) {
|
||||
$permissions = $permissionsArray;
|
||||
foreach ($permissions as $key => $value) {
|
||||
$currentValues = $get($key) ?? [];
|
||||
$allValues = array_unique(array_merge($currentValues, $value));
|
||||
$allValues = array_unique($value);
|
||||
$set($key, $allValues);
|
||||
}
|
||||
}),
|
||||
@ -138,247 +105,7 @@ class ListUsers extends ListRecords
|
||||
]),
|
||||
Tabs::make()
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Tab::make('Console')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.control_desc'))
|
||||
->icon('tabler-terminal-2')
|
||||
->schema([
|
||||
CheckboxList::make('control')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'console' => 'Console',
|
||||
'start' => 'Start',
|
||||
'stop' => 'Stop',
|
||||
'restart' => 'Restart',
|
||||
])
|
||||
->descriptions([
|
||||
'console' => trans('server/users.permissions.control_console'),
|
||||
'start' => trans('server/users.permissions.control_start'),
|
||||
'stop' => trans('server/users.permissions.control_stop'),
|
||||
'restart' => trans('server/users.permissions.control_restart'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('User')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.user_desc'))
|
||||
->icon('tabler-users')
|
||||
->schema([
|
||||
CheckboxList::make('user')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.user_create'),
|
||||
'read' => trans('server/users.permissions.user_read'),
|
||||
'update' => trans('server/users.permissions.user_update'),
|
||||
'delete' => trans('server/users.permissions.user_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('File')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.file_desc'))
|
||||
->icon('tabler-folders')
|
||||
->schema([
|
||||
CheckboxList::make('file')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'read-content' => 'Read Content',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'archive' => 'Archive',
|
||||
'sftp' => 'SFTP',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.file_create'),
|
||||
'read' => trans('server/users.permissions.file_read'),
|
||||
'read-content' => trans('server/users.permissions.file_read_content'),
|
||||
'update' => trans('server/users.permissions.file_update'),
|
||||
'delete' => trans('server/users.permissions.file_delete'),
|
||||
'archive' => trans('server/users.permissions.file_archive'),
|
||||
'sftp' => trans('server/users.permissions.file_sftp'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Backup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.backup_desc'))
|
||||
->icon('tabler-download')
|
||||
->schema([
|
||||
CheckboxList::make('backup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'delete' => 'Delete',
|
||||
'download' => 'Download',
|
||||
'restore' => 'Restore',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.backup_create'),
|
||||
'read' => trans('server/users.permissions.backup_read'),
|
||||
'delete' => trans('server/users.permissions.backup_delete'),
|
||||
'download' => trans('server/users.permissions.backup_download'),
|
||||
'restore' => trans('server/users.permissions.backup_restore'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Allocation')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.allocation_desc'))
|
||||
->icon('tabler-network')
|
||||
->schema([
|
||||
CheckboxList::make('allocation')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.allocation_read'),
|
||||
'create' => trans('server/users.permissions.allocation_create'),
|
||||
'update' => trans('server/users.permissions.allocation_update'),
|
||||
'delete' => trans('server/users.permissions.allocation_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Startup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.startup_desc'))
|
||||
->icon('tabler-question-mark')
|
||||
->schema([
|
||||
CheckboxList::make('startup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'update' => 'Update',
|
||||
'docker-image' => 'Docker Image',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.startup_read'),
|
||||
'update' => trans('server/users.permissions.startup_update'),
|
||||
'docker-image' => trans('server/users.permissions.startup_docker_image'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Database')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.database_desc'))
|
||||
->icon('tabler-database')
|
||||
->schema([
|
||||
CheckboxList::make('database')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'view_password' => 'View Password',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.database_read'),
|
||||
'create' => trans('server/users.permissions.database_create'),
|
||||
'update' => trans('server/users.permissions.database_update'),
|
||||
'delete' => trans('server/users.permissions.database_delete'),
|
||||
'view_password' => trans('server/users.permissions.database_view_password'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Schedule')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.schedule_desc'))
|
||||
->icon('tabler-clock')
|
||||
->schema([
|
||||
CheckboxList::make('schedule')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.schedule_read'),
|
||||
'create' => trans('server/users.permissions.schedule_create'),
|
||||
'update' => trans('server/users.permissions.schedule_update'),
|
||||
'delete' => trans('server/users.permissions.schedule_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Settings')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.settings_desc'))
|
||||
->icon('tabler-settings')
|
||||
->schema([
|
||||
CheckboxList::make('settings')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'rename' => 'Rename',
|
||||
'reinstall' => 'Reinstall',
|
||||
])
|
||||
->descriptions([
|
||||
'rename' => trans('server/users.permissions.setting_rename'),
|
||||
'reinstall' => trans('server/users.permissions.setting_reinstall'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Activity')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.activity_desc'))
|
||||
->icon('tabler-stack')
|
||||
->schema([
|
||||
CheckboxList::make('activity')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->columns(2)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.activity_read'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
->schema($tabs),
|
||||
]),
|
||||
])
|
||||
->modalHeading('Invite User')
|
||||
|
@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
@ -16,10 +17,19 @@ class ServerCpuChart extends ChartWidget
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
public static function canView(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
|
||||
}
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
|
||||
$cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))
|
||||
->slice(-10)
|
||||
->slice(-$period)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => Number::format($value, maxPrecision: 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
|
@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
@ -16,9 +17,19 @@ class ServerMemoryChart extends ChartWidget
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
public static function canView(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
|
||||
}
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->slice(-10)
|
||||
$period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
|
||||
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))
|
||||
->slice(-$period)
|
||||
->map(fn ($value, $key) => [
|
||||
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
|
@ -4,61 +4,72 @@ namespace App\Filament\Server\Widgets;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
|
||||
class ServerNetworkChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Network';
|
||||
|
||||
protected static ?string $pollingInterval = '1s';
|
||||
|
||||
protected static ?string $maxHeight = '300px';
|
||||
protected static ?string $maxHeight = '200px';
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
public static function canView(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
|
||||
}
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
$data = cache()->get("servers.{$this->server->id}.network");
|
||||
$previous = null;
|
||||
|
||||
$rx = collect($data)
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'rx' => $value->rx_bytes,
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
$period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
|
||||
$net = collect(cache()->get("servers.{$this->server->id}.network"))
|
||||
->slice(-$period)
|
||||
->map(function ($current, $timestamp) use (&$previous) {
|
||||
$net = null;
|
||||
|
||||
$tx = collect($data)
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'tx' => $value->rx_bytes,
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
if ($previous !== null) {
|
||||
$net = [
|
||||
'rx' => max(0, $current->rx_bytes - $previous->rx_bytes),
|
||||
'tx' => max(0, $current->tx_bytes - $previous->tx_bytes),
|
||||
'timestamp' => Carbon::createFromTimestamp($timestamp, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
];
|
||||
}
|
||||
|
||||
$previous = $current;
|
||||
|
||||
return $net;
|
||||
})
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Inbound',
|
||||
'data' => array_column($rx, 'rx'),
|
||||
'data' => array_column($net, 'rx'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
'rgba(100, 255, 105, 0.5)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
[
|
||||
'label' => 'Outbound',
|
||||
'data' => array_column($tx, 'tx'),
|
||||
'data' => array_column($net, 'tx'),
|
||||
'backgroundColor' => [
|
||||
'rgba(165, 96, 250, 0.3)',
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($rx, 'timestamp'),
|
||||
'labels' => array_column($net, 'timestamp'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -69,25 +80,38 @@ class ServerNetworkChart extends ChartWidget
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
// TODO: use "panel.use_binary_prefix" config value
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: true,
|
||||
},
|
||||
display: false, //debug
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
ticks: {
|
||||
display: true,
|
||||
callback(value) {
|
||||
const bytes = typeof value === 'string' ? parseInt(value, 10) : value;
|
||||
|
||||
if (bytes < 1) return '0 Bytes';
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const number = Number((bytes / Math.pow(1024, i)).toFixed(2));
|
||||
|
||||
return `${number} ${['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'][i]}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$lastData = collect(cache()->get("servers.{$this->server->id}.network"))->last();
|
||||
|
||||
return 'Network - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0);
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ use App\Enums\ContainerStatus;
|
||||
use App\Filament\Server\Components\SmallStatBlock;
|
||||
use App\Models\Server;
|
||||
use Carbon\CarbonInterface;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\StatsOverviewWidget;
|
||||
use Illuminate\Support\Number;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class ServerOverview extends StatsOverviewWidget
|
||||
{
|
||||
@ -19,14 +21,10 @@ class ServerOverview extends StatsOverviewWidget
|
||||
{
|
||||
return [
|
||||
SmallStatBlock::make('Name', $this->server->name)
|
||||
->extraAttributes([
|
||||
'class' => 'overflow-x-auto',
|
||||
]),
|
||||
->copyOnClick(fn () => request()->isSecure()),
|
||||
SmallStatBlock::make('Status', $this->status()),
|
||||
SmallStatBlock::make('Address', $this->server->allocation->address)
|
||||
->extraAttributes([
|
||||
'class' => 'overflow-x-auto',
|
||||
]),
|
||||
->copyOnClick(fn () => request()->isSecure()),
|
||||
SmallStatBlock::make('CPU', $this->cpuUsage()),
|
||||
SmallStatBlock::make('Memory', $this->memoryUsage()),
|
||||
SmallStatBlock::make('Disk', $this->diskUsage()),
|
||||
@ -93,4 +91,16 @@ class ServerOverview extends StatsOverviewWidget
|
||||
|
||||
return $used . ($this->server->disk > 0 ? ' / ' . $total : ' / ∞');
|
||||
}
|
||||
|
||||
#[On('copyClick')]
|
||||
public function copyClick(string $value): void
|
||||
{
|
||||
$this->js("window.navigator.clipboard.writeText('{$value}');");
|
||||
|
||||
Notification::make()
|
||||
->title('Copied to clipboard')
|
||||
->body($value)
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ class NetworkAllocationController extends ClientApiController
|
||||
if ($original !== $allocation->notes) {
|
||||
Activity::event('server:allocation.notes')
|
||||
->subject($allocation)
|
||||
->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes])
|
||||
->property(['allocation' => $allocation->address, 'old' => $original, 'new' => $allocation->notes])
|
||||
->log();
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ class NetworkAllocationController extends ClientApiController
|
||||
|
||||
Activity::event('server:allocation.primary')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
@ -114,7 +114,7 @@ class NetworkAllocationController extends ClientApiController
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
@ -148,7 +148,7 @@ class NetworkAllocationController extends ClientApiController
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
|
@ -9,6 +9,7 @@ use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Http\Requests\Api\Client\Servers\SendPowerRequest;
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
|
||||
#[Group('Server', weight: 2)]
|
||||
class PowerController extends ClientApiController
|
||||
@ -25,6 +26,8 @@ class PowerController extends ClientApiController
|
||||
* Send power action
|
||||
*
|
||||
* Send a power action to a server.
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function index(SendPowerRequest $request, Server $server): Response
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\DescriptionServerRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest;
|
||||
@ -34,25 +35,33 @@ class SettingsController extends ClientApiController
|
||||
public function rename(RenameServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
||||
|
||||
$server->name = $name;
|
||||
$server->update(['name' => $name]);
|
||||
|
||||
if (config('panel.editable_server_descriptions')) {
|
||||
$server->description = $description;
|
||||
}
|
||||
|
||||
$server->save();
|
||||
|
||||
if ($server->name !== $name) {
|
||||
if ($server->wasChanged('name')) {
|
||||
Activity::event('server:settings.rename')
|
||||
->property(['old' => $server->name, 'new' => $name])
|
||||
->property(['old' => $server->getOriginal('name'), 'new' => $name])
|
||||
->log();
|
||||
}
|
||||
|
||||
if ($server->description !== $description) {
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update server description
|
||||
*/
|
||||
public function description(DescriptionServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!config('panel.editable_server_descriptions')) {
|
||||
return new JsonResponse([], Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$description = $request->input('description');
|
||||
$server->update(['description' => $description ?? '']);
|
||||
|
||||
if ($server->wasChanged('description')) {
|
||||
Activity::event('server:settings.description')
|
||||
->property(['old' => $server->description, 'new' => $description])
|
||||
->property(['old' => $server->getOriginal('description'), 'new' => $description])
|
||||
->log();
|
||||
}
|
||||
|
||||
@ -84,7 +93,7 @@ class SettingsController extends ClientApiController
|
||||
*/
|
||||
public function dockerImage(SetDockerImageRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!in_array($server->image, array_values($server->egg->docker_images))) {
|
||||
if (!in_array($server->image, $server->egg->docker_images)) {
|
||||
throw new BadRequestHttpException('This server\'s Docker image has been manually set by an administrator and cannot be updated.');
|
||||
}
|
||||
|
||||
|
@ -18,26 +18,7 @@ class StoreNodeRequest extends ApplicationApiRequest
|
||||
*/
|
||||
public function rules(?array $rules = null): array
|
||||
{
|
||||
return collect($rules ?? Node::getRules())->only([
|
||||
'public',
|
||||
'name',
|
||||
'description',
|
||||
'fqdn',
|
||||
'scheme',
|
||||
'behind_proxy',
|
||||
'maintenance_mode',
|
||||
'memory',
|
||||
'memory_overallocate',
|
||||
'disk',
|
||||
'disk_overallocate',
|
||||
'cpu',
|
||||
'cpu_overallocate',
|
||||
'upload_size',
|
||||
'daemon_listen',
|
||||
'daemon_sftp',
|
||||
'daemon_sftp_alias',
|
||||
'daemon_base',
|
||||
])->mapWithKeys(function ($value, $key) {
|
||||
return collect($rules ?? Node::getRules())->mapWithKeys(function ($value, $key) {
|
||||
return [snake_case($key) => $value];
|
||||
})->toArray();
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Client\Servers\Settings;
|
||||
|
||||
use App\Contracts\Http\ClientPermissionsRequest;
|
||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use App\Models\Permission;
|
||||
|
||||
class DescriptionServerRequest extends ClientApiRequest implements ClientPermissionsRequest
|
||||
{
|
||||
/**
|
||||
* Returns the permissions string indicating which permission should be used to
|
||||
* validate that the authenticated user has permission to perform this action against
|
||||
* the given resource (server).
|
||||
*/
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_SETTINGS_DESCRIPTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* The rules to apply when validating this request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'description' => 'string|nullable',
|
||||
];
|
||||
}
|
||||
}
|
@ -26,7 +26,6 @@ class RenameServerRequest extends ClientApiRequest implements ClientPermissionsR
|
||||
{
|
||||
return [
|
||||
'name' => Server::getRules()['name'],
|
||||
'description' => 'string|nullable',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ abstract class SubuserRequest extends ClientApiRequest
|
||||
$server = $this->route()->parameter('server');
|
||||
|
||||
// If we are an admin or the server owner, no need to perform these checks.
|
||||
if ($user->can('update server', $server) || $user->id === $server->owner_id) {
|
||||
if ($user->can('update', $server) || $user->id === $server->owner_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NodeStatistics implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
foreach (Node::all() as $node) {
|
||||
$stats = $node->statistics();
|
||||
$timestamp = now()->getTimestamp();
|
||||
|
||||
foreach ($stats as $key => $value) {
|
||||
$cacheKey = "nodes.{$node->id}.$key";
|
||||
$data = cache()->get($cacheKey, []);
|
||||
|
||||
// Add current timestamp and value to the data array
|
||||
$data[$timestamp] = $value;
|
||||
|
||||
// Update the cache with the new data, expires in 1 minute
|
||||
cache()->put($cacheKey, $data, now()->addMinute());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ namespace App\Listeners\Auth;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use App\Events\Auth\DirectLogin;
|
||||
use Illuminate\Auth\Events\Login;
|
||||
|
||||
class AuthenticationListener
|
||||
{
|
||||
@ -12,9 +12,10 @@ class AuthenticationListener
|
||||
* Handles an authentication event by logging the user and information about
|
||||
* the request.
|
||||
*/
|
||||
public function handle(Failed|DirectLogin $event): void
|
||||
public function handle(Failed|Login $event): void
|
||||
{
|
||||
$activity = Activity::withRequestMetadata();
|
||||
|
||||
if ($event->user) {
|
||||
$activity = $activity->subject($event->user);
|
||||
}
|
||||
|
@ -2,22 +2,14 @@
|
||||
|
||||
namespace App\Listeners\Auth;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Facades\Activity;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
|
||||
class PasswordResetListener
|
||||
{
|
||||
protected Request $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function handle(PasswordReset $event): void
|
||||
{
|
||||
Activity::event('event:password-reset')
|
||||
Activity::event('auth:password-reset')
|
||||
->withRequestMetadata()
|
||||
->subject($event->user)
|
||||
->log();
|
||||
|
64
app/Livewire/ServerEntry.php
Normal file
64
app/Livewire/ServerEntry.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class ServerEntry extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.server-entry');
|
||||
}
|
||||
|
||||
public function placeholder(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute left-0 top-1 bottom-0 w-1 rounded-lg"
|
||||
style="background-color: #D97706;">
|
||||
</div>
|
||||
|
||||
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
|
||||
<div class="flex items-center mb-5 gap-2">
|
||||
<x-filament::loading-indicator class="h-5 w-5" />
|
||||
<h2 class="text-xl font-bold">
|
||||
{{ $server->name }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-center">
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">CPU</p>
|
||||
<p class="text-md font-semibold">{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">Memory</p>
|
||||
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('memory', limit: true) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">Disk</p>
|
||||
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('disk', limit: true) }}</p>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<p class="text-sm dark:text-gray-400">Network</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-md font-semibold">{{ $server->allocation->address }} </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
@ -117,15 +117,10 @@ class Allocation extends Model
|
||||
protected function address(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => "$this->alias:$this->port",
|
||||
get: fn () => (is_ipv6($this->alias) ? "[$this->alias]" : $this->alias) . ":$this->port",
|
||||
);
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the server associated with this allocation.
|
||||
*/
|
||||
|
@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@ -283,6 +284,11 @@ class Egg extends Model implements Validatable
|
||||
return $this->configFrom->file_denylist;
|
||||
}
|
||||
|
||||
public function mounts(): MorphToMany
|
||||
{
|
||||
return $this->morphToMany(Mount::class, 'mountable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all servers associated with this egg.
|
||||
*/
|
||||
|
@ -12,6 +12,9 @@ use Illuminate\Http\Client\ConnectionException;
|
||||
use Sushi\Sushi;
|
||||
|
||||
/**
|
||||
* \App\Models\File.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $modified_at
|
||||
@ -27,12 +30,6 @@ class File extends Model
|
||||
{
|
||||
use Sushi;
|
||||
|
||||
protected $primaryKey = 'name';
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected int $sushiInsertChunkSize = 100;
|
||||
|
||||
public const ARCHIVE_MIMES = [
|
||||
@ -153,16 +150,10 @@ class File extends Model
|
||||
try {
|
||||
$fileRepository = (new DaemonFileRepository())->setServer(self::$server);
|
||||
|
||||
$contents = [];
|
||||
|
||||
try {
|
||||
if (!is_null(self::$searchTerm)) {
|
||||
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
|
||||
} else {
|
||||
$contents = $fileRepository->getDirectory(self::$path ?? '/');
|
||||
}
|
||||
} catch (ConnectionException $exception) {
|
||||
report($exception);
|
||||
if (!is_null(self::$searchTerm)) {
|
||||
$contents = cache()->remember('file_search_' . self::$path . '_' . self::$searchTerm, now()->addMinute(), fn () => $fileRepository->search(self::$searchTerm, self::$path));
|
||||
} else {
|
||||
$contents = $fileRepository->getDirectory(self::$path ?? '/');
|
||||
}
|
||||
|
||||
if (isset($contents['error'])) {
|
||||
@ -199,8 +190,12 @@ class File extends Model
|
||||
$message = $message->after('cURL error 7: ')->before(' after ');
|
||||
}
|
||||
|
||||
if ($exception instanceof ConnectionException) {
|
||||
$message = str('Node connection failed');
|
||||
}
|
||||
|
||||
AlertBanner::make()
|
||||
->title('Could not load files')
|
||||
->title('Could not load files!')
|
||||
->body($message->toString())
|
||||
->danger()
|
||||
->send();
|
||||
|
@ -5,7 +5,7 @@ namespace App\Models;
|
||||
use App\Contracts\Validatable;
|
||||
use App\Traits\HasValidation;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@ -102,24 +102,24 @@ class Mount extends Model implements Validatable
|
||||
/**
|
||||
* Returns all eggs that have this mount assigned.
|
||||
*/
|
||||
public function eggs(): BelongsToMany
|
||||
public function eggs(): MorphToMany
|
||||
{
|
||||
return $this->belongsToMany(Egg::class);
|
||||
return $this->morphedByMany(Egg::class, 'mountable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all nodes that have this mount assigned.
|
||||
*/
|
||||
public function nodes(): BelongsToMany
|
||||
public function nodes(): MorphToMany
|
||||
{
|
||||
return $this->belongsToMany(Node::class);
|
||||
return $this->morphedByMany(Node::class, 'mountable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all servers that have this mount assigned.
|
||||
*/
|
||||
public function servers(): BelongsToMany
|
||||
public function servers(): MorphToMany
|
||||
{
|
||||
return $this->belongsToMany(Server::class);
|
||||
return $this->morphedByMany(Server::class, 'mountable');
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MountNode extends Model
|
||||
{
|
||||
protected $table = 'mount_node';
|
||||
|
||||
protected $primaryKey = null;
|
||||
|
||||
public $incrementing = false;
|
||||
}
|
@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
@ -37,6 +38,7 @@ use Symfony\Component\Yaml\Yaml;
|
||||
* @property string $daemon_token_id
|
||||
* @property string $daemon_token
|
||||
* @property int $daemon_listen
|
||||
* @property int $daemon_connect
|
||||
* @property int $daemon_sftp
|
||||
* @property string|null $daemon_sftp_alias
|
||||
* @property string $daemon_base
|
||||
@ -49,6 +51,8 @@ use Symfony\Component\Yaml\Yaml;
|
||||
* @property int|null $servers_count
|
||||
* @property \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
|
||||
* @property int|null $allocations_count
|
||||
* @property \App\Models\Role[]|\Illuminate\Database\Eloquent\Collection $roles
|
||||
* @property int|null $roles_count
|
||||
*/
|
||||
class Node extends Model implements Validatable
|
||||
{
|
||||
@ -80,7 +84,7 @@ class Node extends Model implements Validatable
|
||||
'memory', 'memory_overallocate', 'disk',
|
||||
'disk_overallocate', 'cpu', 'cpu_overallocate',
|
||||
'upload_size', 'daemon_base',
|
||||
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen',
|
||||
'daemon_sftp', 'daemon_sftp_alias', 'daemon_listen', 'daemon_connect',
|
||||
'description', 'maintenance_mode', 'tags',
|
||||
];
|
||||
|
||||
@ -102,6 +106,7 @@ class Node extends Model implements Validatable
|
||||
'daemon_sftp' => ['required', 'numeric', 'between:1,65535'],
|
||||
'daemon_sftp_alias' => ['nullable', 'string'],
|
||||
'daemon_listen' => ['required', 'numeric', 'between:1,65535'],
|
||||
'daemon_connect' => ['required', 'numeric', 'between:1,65535'],
|
||||
'maintenance_mode' => ['boolean'],
|
||||
'upload_size' => ['int', 'between:1,1024'],
|
||||
'tags' => ['array'],
|
||||
@ -122,6 +127,7 @@ class Node extends Model implements Validatable
|
||||
'daemon_base' => '/var/lib/pelican/volumes',
|
||||
'daemon_sftp' => 2022,
|
||||
'daemon_listen' => 8080,
|
||||
'daemon_connect' => 8080,
|
||||
'maintenance_mode' => false,
|
||||
'tags' => '[]',
|
||||
];
|
||||
@ -133,6 +139,7 @@ class Node extends Model implements Validatable
|
||||
'disk' => 'integer',
|
||||
'cpu' => 'integer',
|
||||
'daemon_listen' => 'integer',
|
||||
'daemon_connect' => 'integer',
|
||||
'daemon_sftp' => 'integer',
|
||||
'daemon_token' => 'encrypted',
|
||||
'behind_proxy' => 'boolean',
|
||||
@ -168,7 +175,7 @@ class Node extends Model implements Validatable
|
||||
*/
|
||||
public function getConnectionAddress(): string
|
||||
{
|
||||
return "$this->scheme://$this->fqdn:$this->daemon_listen";
|
||||
return "$this->scheme://$this->fqdn:$this->daemon_connect";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -214,7 +221,7 @@ class Node extends Model implements Validatable
|
||||
],
|
||||
],
|
||||
'allowed_mounts' => $this->mounts->pluck('source')->toArray(),
|
||||
'remote' => route('filament.app.resources...index'),
|
||||
'remote' => config('app.url'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -239,9 +246,9 @@ class Node extends Model implements Validatable
|
||||
return $this->maintenance_mode;
|
||||
}
|
||||
|
||||
public function mounts(): HasManyThrough
|
||||
public function mounts(): MorphToMany
|
||||
{
|
||||
return $this->hasManyThrough(Mount::class, MountNode::class, 'node_id', 'id', 'id', 'mount_id');
|
||||
return $this->morphToMany(Mount::class, 'mountable');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -268,6 +275,11 @@ class Node extends Model implements Validatable
|
||||
return $this->belongsToMany(DatabaseHost::class);
|
||||
}
|
||||
|
||||
public function roles(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(Role::class, NodeRole::class, 'node_id', 'id', 'id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean if the node is viable for an additional server to be placed on it.
|
||||
*/
|
||||
@ -396,10 +408,11 @@ class Node extends Model implements Validatable
|
||||
}
|
||||
}
|
||||
|
||||
// Only IPV4
|
||||
$ips = $ips->filter(fn (string $ip) => filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false);
|
||||
$ips = $ips->filter(fn (string $ip) => is_ip($ip));
|
||||
|
||||
// TODO: remove later
|
||||
$ips->push('0.0.0.0');
|
||||
$ips->push('::');
|
||||
|
||||
return $ips->unique()->all();
|
||||
});
|
||||
|
12
app/Models/NodeRole.php
Normal file
12
app/Models/NodeRole.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
||||
class NodeRole extends Pivot
|
||||
{
|
||||
protected $table = 'node_role';
|
||||
|
||||
protected $primaryKey = null;
|
||||
}
|
@ -4,12 +4,13 @@ namespace App\Models;
|
||||
|
||||
use App\Contracts\Validatable;
|
||||
use App\Traits\HasValidation;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Permission extends Model implements Validatable
|
||||
{
|
||||
use HasValidation;
|
||||
use HasFactory, HasValidation;
|
||||
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
@ -38,7 +39,7 @@ class Permission extends Model implements Validatable
|
||||
|
||||
public const ACTION_DATABASE_DELETE = 'database.delete';
|
||||
|
||||
public const ACTION_DATABASE_VIEW_PASSWORD = 'database.view_password';
|
||||
public const ACTION_DATABASE_VIEW_PASSWORD = 'database.view-password';
|
||||
|
||||
public const ACTION_SCHEDULE_READ = 'schedule.read';
|
||||
|
||||
@ -96,6 +97,8 @@ class Permission extends Model implements Validatable
|
||||
|
||||
public const ACTION_SETTINGS_RENAME = 'settings.rename';
|
||||
|
||||
public const ACTION_SETTINGS_DESCRIPTION = 'settings.description';
|
||||
|
||||
public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall';
|
||||
|
||||
public const ACTION_ACTIVITY_READ = 'activity.read';
|
||||
@ -113,127 +116,6 @@ class Permission extends Model implements Validatable
|
||||
'permission' => ['required', 'string'],
|
||||
];
|
||||
|
||||
/**
|
||||
* All the permissions available on the system. You should use self::permissions()
|
||||
* to retrieve them, and not directly access this array as it is subject to change.
|
||||
*
|
||||
* @see Permission::permissions()
|
||||
*
|
||||
* @var array<array-key, array{
|
||||
* description: string,
|
||||
* keys: array<array-key, string>,
|
||||
* }>
|
||||
*/
|
||||
protected static array $permissions = [
|
||||
'websocket' => [
|
||||
'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.',
|
||||
'keys' => [
|
||||
'connect' => 'Allows a user to connect to the websocket instance for a server to stream the console.',
|
||||
],
|
||||
],
|
||||
|
||||
'control' => [
|
||||
'description' => 'Permissions that control a user\'s ability to control the power state of a server, or send commands.',
|
||||
'keys' => [
|
||||
'console' => 'Allows a user to send commands to the server instance via the console.',
|
||||
'start' => 'Allows a user to start the server if it is stopped.',
|
||||
'stop' => 'Allows a user to stop a server if it is running.',
|
||||
'restart' => 'Allows a user to perform a server restart. This allows them to start the server if it is offline, but not put the server in a completely stopped state.',
|
||||
],
|
||||
],
|
||||
|
||||
'user' => [
|
||||
'description' => 'Permissions that allow a user to manage other subusers on a server. They will never be able to edit their own account, or assign permissions they do not have themselves.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new subusers for the server.',
|
||||
'read' => 'Allows the user to view subusers and their permissions for the server.',
|
||||
'update' => 'Allows a user to modify other subusers.',
|
||||
'delete' => 'Allows a user to delete a subuser from the server.',
|
||||
],
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'description' => 'Permissions that control a user\'s ability to modify the filesystem for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create additional files and folders via the Panel or direct upload.',
|
||||
'read' => 'Allows a user to view the contents of a directory, but not view the contents of or download files.',
|
||||
'read-content' => 'Allows a user to view the contents of a given file. This will also allow the user to download files.',
|
||||
'update' => 'Allows a user to update the contents of an existing file or directory.',
|
||||
'delete' => 'Allows a user to delete files or directories.',
|
||||
'archive' => 'Allows a user to archive the contents of a directory as well as decompress existing archives on the system.',
|
||||
'sftp' => 'Allows a user to connect to SFTP and manage server files using the other assigned file permissions.',
|
||||
],
|
||||
],
|
||||
|
||||
'backup' => [
|
||||
'description' => 'Permissions that control a user\'s ability to generate and manage server backups.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new backups for this server.',
|
||||
'read' => 'Allows a user to view all backups that exist for this server.',
|
||||
'delete' => 'Allows a user to remove backups from the system.',
|
||||
'download' => 'Allows a user to download a backup for the server. Danger: this allows a user to access all files for the server in the backup.',
|
||||
'restore' => 'Allows a user to restore a backup for the server. Danger: this allows the user to delete all the server files in the process.',
|
||||
],
|
||||
],
|
||||
|
||||
// Controls permissions for editing or viewing a server's allocations.
|
||||
'allocation' => [
|
||||
'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view all allocations currently assigned to this server. Users with any level of access to this server can always view the primary allocation.',
|
||||
'create' => 'Allows a user to assign additional allocations to the server.',
|
||||
'update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.',
|
||||
'delete' => 'Allows a user to delete an allocation from the server.',
|
||||
],
|
||||
],
|
||||
|
||||
// Controls permissions for editing or viewing a server's startup parameters.
|
||||
'startup' => [
|
||||
'description' => 'Permissions that control a user\'s ability to view this server\'s startup parameters.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view the startup variables for a server.',
|
||||
'update' => 'Allows a user to modify the startup variables for the server.',
|
||||
'docker-image' => 'Allows a user to modify the Docker image used when running the server.',
|
||||
],
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'description' => 'Permissions that control a user\'s access to the database management for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create a new database for this server.',
|
||||
'read' => 'Allows a user to view the database associated with this server.',
|
||||
'update' => 'Allows a user to rotate the password on a database instance. If the user does not have the view_password permission they will not see the updated password.',
|
||||
'delete' => 'Allows a user to remove a database instance from this server.',
|
||||
'view_password' => 'Allows a user to view the password associated with a database instance for this server.',
|
||||
],
|
||||
],
|
||||
|
||||
'schedule' => [
|
||||
'description' => 'Permissions that control a user\'s access to the schedule management for this server.',
|
||||
'keys' => [
|
||||
'create' => 'Allows a user to create new schedules for this server.', // task.create-schedule
|
||||
'read' => 'Allows a user to view schedules and the tasks associated with them for this server.', // task.view-schedule, task.list-schedules
|
||||
'update' => 'Allows a user to update schedules and schedule tasks for this server.', // task.edit-schedule, task.queue-schedule, task.toggle-schedule
|
||||
'delete' => 'Allows a user to delete schedules for this server.', // task.delete-schedule
|
||||
],
|
||||
],
|
||||
|
||||
'settings' => [
|
||||
'description' => 'Permissions that control a user\'s access to the settings for this server.',
|
||||
'keys' => [
|
||||
'rename' => 'Allows a user to rename this server and change the description of it.',
|
||||
'reinstall' => 'Allows a user to trigger a reinstall of this server.',
|
||||
],
|
||||
],
|
||||
|
||||
'activity' => [
|
||||
'description' => 'Permissions that control a user\'s access to the server activity logs.',
|
||||
'keys' => [
|
||||
'read' => 'Allows a user to view the activity logs for the server.',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
@ -241,11 +123,92 @@ class Permission extends Model implements Validatable
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* All the permissions available on the system.
|
||||
*
|
||||
* @return array<int, array{
|
||||
* name: string,
|
||||
* icon: string,
|
||||
* permissions: string[]
|
||||
* }>
|
||||
*/
|
||||
public static function permissionData(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'control',
|
||||
'icon' => 'tabler-terminal-2',
|
||||
'permissions' => ['console', 'start', 'stop', 'restart'],
|
||||
],
|
||||
[
|
||||
'name' => 'user',
|
||||
'icon' => 'tabler-users',
|
||||
'permissions' => ['read', 'create', 'update', 'delete'],
|
||||
],
|
||||
[
|
||||
'name' => 'file',
|
||||
'icon' => 'tabler-files',
|
||||
'permissions' => ['read', 'read-content', 'create', 'update', 'delete', 'archive', 'sftp'],
|
||||
],
|
||||
[
|
||||
'name' => 'backup',
|
||||
'icon' => 'tabler-file-zip',
|
||||
'permissions' => ['read', 'create', 'delete', 'download', 'restore'],
|
||||
],
|
||||
[
|
||||
'name' => 'allocation',
|
||||
'icon' => 'tabler-network',
|
||||
'permissions' => ['read', 'create', 'update', 'delete'],
|
||||
],
|
||||
[
|
||||
'name' => 'startup',
|
||||
'icon' => 'tabler-player-play',
|
||||
'permissions' => ['read', 'update', 'docker-image'],
|
||||
],
|
||||
[
|
||||
'name' => 'database',
|
||||
'icon' => 'tabler-database',
|
||||
'permissions' => ['read', 'create', 'update', 'delete', 'view-password'],
|
||||
],
|
||||
[
|
||||
'name' => 'schedule',
|
||||
'icon' => 'tabler-clock',
|
||||
'permissions' => ['read', 'create', 'update', 'delete'],
|
||||
],
|
||||
[
|
||||
'name' => 'settings',
|
||||
'icon' => 'tabler-settings',
|
||||
'permissions' => ['rename', 'description', 'reinstall'],
|
||||
],
|
||||
[
|
||||
'name' => 'activity',
|
||||
'icon' => 'tabler-stack',
|
||||
'permissions' => ['read'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the permissions available on the system for a user to have when controlling a server.
|
||||
*/
|
||||
public static function permissions(): Collection
|
||||
{
|
||||
return Collection::make(self::$permissions);
|
||||
$permissions = [
|
||||
'websocket' => [
|
||||
'description' => 'Allows the user to connect to the server websocket, giving them access to view console output and realtime server stats.',
|
||||
'keys' => [
|
||||
'connect' => 'Allows a user to connect to the websocket instance for a server to stream the console.',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach (static::permissionData() as $data) {
|
||||
$permissions[$data['name']] = [
|
||||
'description' => trans('server/users.permissions.' . $data['name'] . '_desc'),
|
||||
'keys' => collect($data['permissions'])->mapWithKeys(fn ($key) => [$key => trans('server/users.permissions.' . $data['name'] . '_' . str($key)->replace('-', '_'))])->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
return collect($permissions);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ namespace App\Models;
|
||||
|
||||
use App\Enums\RolePermissionModels;
|
||||
use App\Enums\RolePermissionPrefixes;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Spatie\Permission\Models\Role as BaseRole;
|
||||
|
||||
/**
|
||||
@ -14,9 +16,13 @@ use Spatie\Permission\Models\Role as BaseRole;
|
||||
* @property int|null $permissions_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\User[] $users
|
||||
* @property int|null $users_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Node[] $nodes
|
||||
* @property int|null $nodes_count
|
||||
*/
|
||||
class Role extends BaseRole
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const RESOURCE_NAME = 'role';
|
||||
|
||||
public const ROOT_ADMIN = 'Root Admin';
|
||||
@ -125,4 +131,9 @@ class Role extends BaseRole
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
public function nodes(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Node::class, NodeRole::class);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ use Carbon\CarbonInterface;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
@ -232,7 +231,12 @@ class Server extends Model implements Validatable
|
||||
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
return $this->status !== ServerState::Installing && $this->status !== ServerState::InstallFailed;
|
||||
return $this->status !== ServerState::Installing && !$this->isFailedInstall();
|
||||
}
|
||||
|
||||
public function isFailedInstall(): bool
|
||||
{
|
||||
return $this->status === ServerState::InstallFailed || $this->status === ServerState::ReinstallFailed;
|
||||
}
|
||||
|
||||
public function isSuspended(): bool
|
||||
@ -350,9 +354,9 @@ class Server extends Model implements Validatable
|
||||
return $this->hasMany(Backup::class);
|
||||
}
|
||||
|
||||
public function mounts(): BelongsToMany
|
||||
public function mounts(): MorphToMany
|
||||
{
|
||||
return $this->belongsToMany(Mount::class);
|
||||
return $this->morphToMany(Mount::class, 'mountable');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -265,8 +265,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
public function accessibleServers(): Builder
|
||||
{
|
||||
if ($this->canned('viewList server')) {
|
||||
return Server::query();
|
||||
if ($this->canned('viewAny', Server::class)) {
|
||||
return Server::select('servers.*')
|
||||
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
|
||||
->where(function (Builder $builder) {
|
||||
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id)->orWhereIn('servers.node_id', $this->accessibleNodes()->pluck('id'));
|
||||
})
|
||||
->distinct('servers.id');
|
||||
}
|
||||
|
||||
return $this->directAccessibleServers();
|
||||
@ -278,14 +283,29 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
public function directAccessibleServers(): Builder
|
||||
{
|
||||
return Server::query()
|
||||
->select('servers.*')
|
||||
return Server::select('servers.*')
|
||||
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
|
||||
->where(function (Builder $builder) {
|
||||
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function accessibleNodes(): Builder
|
||||
{
|
||||
// Root admins can access all nodes
|
||||
if ($this->isRootAdmin()) {
|
||||
return Node::query();
|
||||
}
|
||||
|
||||
// Check if there are no restrictions from any role
|
||||
$roleIds = $this->roles()->pluck('id');
|
||||
if (!NodeRole::whereIn('role_id', $roleIds)->exists()) {
|
||||
return Node::query();
|
||||
}
|
||||
|
||||
return Node::whereHas('roles', fn (Builder $builder) => $builder->whereIn('roles.id', $roleIds));
|
||||
}
|
||||
|
||||
public function subusers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Subuser::class);
|
||||
@ -298,12 +318,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
protected function checkPermission(Server $server, string $permission = ''): bool
|
||||
{
|
||||
if ($this->canned('update server', $server) || $server->owner_id === $this->id) {
|
||||
if ($this->canned('update', $server) || $server->owner_id === $this->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the user only has "view" permissions allow viewing the console
|
||||
if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view server', $server)) {
|
||||
if ($permission === Permission::ACTION_WEBSOCKET_CONNECT && $this->canned('view', $server)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -390,13 +410,24 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
return $provider?->get($this);
|
||||
}
|
||||
|
||||
public function canTarget(Model $user): bool
|
||||
public function canTarget(Model $model): bool
|
||||
{
|
||||
// Root admins can target everyone and everything
|
||||
if ($this->isRootAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user instanceof User && !$user->isRootAdmin();
|
||||
// Make sure normal admins can't target root admins
|
||||
if ($model instanceof User) {
|
||||
return !$model->isRootAdmin();
|
||||
}
|
||||
|
||||
// Make sure the user can only target accessible nodes
|
||||
if ($model instanceof Node) {
|
||||
return $this->accessibleNodes()->where('id', $model->id)->exists();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getTenants(Panel $panel): array|Collection
|
||||
@ -407,7 +438,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
public function canAccessTenant(Model $tenant): bool
|
||||
{
|
||||
if ($tenant instanceof Server) {
|
||||
if ($this->canned('view server', $tenant) || $tenant->owner_id === $this->id) {
|
||||
if ($this->canned('view', $tenant) || $tenant->owner_id === $this->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ class AccountCreated extends Notification implements ShouldQueue
|
||||
->line('Email: ' . $notifiable->email);
|
||||
|
||||
if (!is_null($this->token)) {
|
||||
return $message->action('Setup Your Account', Filament::getResetPasswordUrl($this->token, $notifiable));
|
||||
return $message->action('Setup Your Account', Filament::getPanel('app')->getResetPasswordUrl($this->token, $notifiable));
|
||||
}
|
||||
|
||||
return $message;
|
||||
|
@ -2,9 +2,28 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\User;
|
||||
|
||||
class DatabaseHostPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'databasehost';
|
||||
|
||||
public function before(User $user, string $ability, string|DatabaseHost $databaseHost): ?bool
|
||||
{
|
||||
// For "viewAny" the $databaseHost param is the class name
|
||||
if (is_string($databaseHost)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($databaseHost->nodes as $node) {
|
||||
if (!$user->canTarget($node)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,28 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Mount;
|
||||
use App\Models\User;
|
||||
|
||||
class MountPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'mount';
|
||||
|
||||
public function before(User $user, string $ability, string|Mount $mount): ?bool
|
||||
{
|
||||
// For "viewAny" the $mount param is the class name
|
||||
if (is_string($mount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($mount->nodes as $node) {
|
||||
if (!$user->canTarget($node)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,26 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Node;
|
||||
use App\Models\User;
|
||||
|
||||
class NodePolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'node';
|
||||
|
||||
public function before(User $user, string $ability, string|Node $node): ?bool
|
||||
{
|
||||
// For "viewAny" the $node param is the class name
|
||||
if (is_string($node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$user->canTarget($node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,11 @@ class ServerPolicy
|
||||
return true;
|
||||
}
|
||||
|
||||
// Make sure user can target node of the server
|
||||
if (!$user->canTarget($server->node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return null to let default policies take over
|
||||
return null;
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
'ssh_key' => Models\UserSSHKey::class,
|
||||
'task' => Models\Task::class,
|
||||
'user' => Models\User::class,
|
||||
'node' => Models\Node::class,
|
||||
]);
|
||||
|
||||
Http::macro(
|
||||
|
@ -22,14 +22,7 @@ class DaemonConfigurationRepository extends DaemonRepository
|
||||
->timeout(1)
|
||||
->get('/api/system')
|
||||
->throwIf(function ($result) {
|
||||
$header = $result->header('User-Agent');
|
||||
if (
|
||||
filled($header) &&
|
||||
preg_match('/^Pelican Wings\/v(?:\d+\.\d+\.\d+|develop) \(id:(\w*)\)$/', $header, $matches) &&
|
||||
array_get($matches, 1, '') !== $this->node->daemon_token_id
|
||||
) {
|
||||
throw new ConnectionException($result->effectiveUri()->__toString() . ' does not match node token_id !');
|
||||
}
|
||||
$this->enforceValidNodeToken($result);
|
||||
if (!$result->collect()->has(['architecture', 'cpu_count', 'kernel_version', 'os', 'version'])) {
|
||||
throw new ConnectionException($result->effectiveUri()->__toString() . ' is not Pelican Wings !');
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Webmozart\Assert\Assert;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\Response;
|
||||
|
||||
abstract class DaemonRepository
|
||||
{
|
||||
@ -47,6 +49,24 @@ abstract class DaemonRepository
|
||||
{
|
||||
Assert::isInstanceOf($this->node, Node::class);
|
||||
|
||||
return Http::daemon($this->node, $headers);
|
||||
return Http::daemon($this->node, $headers)->throwIf(fn ($condition) => $this->enforceValidNodeToken($condition));
|
||||
}
|
||||
|
||||
protected function enforceValidNodeToken(Response|bool $condition): bool
|
||||
{
|
||||
if (is_bool($condition)) {
|
||||
return $condition;
|
||||
}
|
||||
|
||||
$header = $condition->header('User-Agent');
|
||||
if (
|
||||
empty($header) ||
|
||||
preg_match('/^Pelican Wings\/v(?:\d+\.\d+\.\d+|develop) \(id:(\w*)\)$/', $header, $matches) &&
|
||||
array_get($matches, 1, '') !== $this->node->daemon_token_id
|
||||
) {
|
||||
throw new ConnectionException($condition->effectiveUri()->__toString() . ' does not match node token_id !');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ class DaemonServerRepository extends DaemonRepository
|
||||
public function getDetails(): array
|
||||
{
|
||||
try {
|
||||
return $this->getHttpClient()->get("/api/servers/{$this->server->uuid}")->throw()->json();
|
||||
return $this->getHttpClient()->connectTimeout(1)->timeout(1)->get("/api/servers/{$this->server->uuid}")->throw()->json();
|
||||
} catch (RequestException $exception) {
|
||||
$cfId = $exception->response->header('Cf-Ray');
|
||||
$cfCache = $exception->response->header('Cf-Cache-Status');
|
||||
@ -141,4 +141,12 @@ class DaemonServerRepository extends DaemonRepository
|
||||
'jtis' => [md5($id . $this->server->uuid)],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getInstallLogs(): string
|
||||
{
|
||||
return $this->getHttpClient()
|
||||
->get("/api/servers/{$this->server->uuid}/install-logs")
|
||||
->throw()
|
||||
->json('data');
|
||||
}
|
||||
}
|
||||
|
@ -51,12 +51,7 @@ class AssignmentService
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: how should we approach supporting IPv6 with this?
|
||||
// gethostbyname only supports IPv4, but the alternative (dns_get_record) returns
|
||||
// an array of records, which is not ideal for this use case, we need a SINGLE
|
||||
// IP to use, not multiple.
|
||||
$underlying = gethostbyname($data['allocation_ip']);
|
||||
$parsed = Network::parse($underlying);
|
||||
$parsed = Network::parse($data['allocation_ip']);
|
||||
} catch (\Exception $exception) {
|
||||
throw new DisplayException("Could not parse provided allocation IP address ({$data['allocation_ip']}): {$exception->getMessage()}", $exception);
|
||||
}
|
||||
|
@ -6,12 +6,11 @@ use App\Extensions\Filesystem\S3Filesystem;
|
||||
use Aws\S3\S3Client;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Backup;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Extensions\Backups\BackupManager;
|
||||
use App\Repositories\Daemon\DaemonBackupRepository;
|
||||
use App\Exceptions\Service\Backup\BackupLockedException;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Exception;
|
||||
|
||||
class DeleteBackupService
|
||||
{
|
||||
@ -48,12 +47,10 @@ class DeleteBackupService
|
||||
$this->connection->transaction(function () use ($backup) {
|
||||
try {
|
||||
$this->daemonBackupRepository->setServer($backup->server)->delete($backup);
|
||||
} catch (ConnectionException $exception) {
|
||||
$previous = $exception->getPrevious();
|
||||
|
||||
} catch (Exception $exception) {
|
||||
// Don't fail the request if the Daemon responds with a 404, just assume the backup
|
||||
// doesn't actually exist and remove its reference from the Panel as well.
|
||||
if (!$previous instanceof ClientException || $previous->getResponse()->getStatusCode() !== Response::HTTP_NOT_FOUND) {
|
||||
if ($exception->getCode() !== Response::HTTP_NOT_FOUND) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
@ -118,11 +118,18 @@ class DatabaseManagementService
|
||||
*/
|
||||
public function delete(Database $database): ?bool
|
||||
{
|
||||
$database->dropDatabase($database->database);
|
||||
$database->dropUser($database->username, $database->remote);
|
||||
$database->flush();
|
||||
return $this->connection->transaction(function () use ($database) {
|
||||
$database->dropDatabase($database->database);
|
||||
$database->dropUser($database->username, $database->remote);
|
||||
$database->flush();
|
||||
|
||||
return $database->delete();
|
||||
Activity::event('server:database.delete')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
|
||||
return $database->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,8 +16,8 @@ class GetUserPermissionsService
|
||||
*/
|
||||
public function handle(Server $server, User $user): array
|
||||
{
|
||||
if ($user->isAdmin() && ($user->can('view server', $server) || $user->can('update server', $server))) {
|
||||
$permissions = $user->can('update server', $server) ? ['*'] : ['websocket.connect', 'backup.read'];
|
||||
if ($user->isAdmin() && ($user->can('view', $server) || $user->can('update', $server))) {
|
||||
$permissions = $user->can('update', $server) ? ['*'] : ['websocket.connect', 'backup.read'];
|
||||
|
||||
$permissions[] = 'admin.websocket.errors';
|
||||
$permissions[] = 'admin.websocket.install';
|
||||
|
@ -9,7 +9,7 @@ class ToggleInstallService
|
||||
{
|
||||
public function handle(Server $server): void
|
||||
{
|
||||
if ($server->status === ServerState::InstallFailed) {
|
||||
if ($server->isFailedInstall()) {
|
||||
abort(500, trans('exceptions.server.marked_as_failed'));
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ class TransferServerService
|
||||
|
||||
// Check if the node is viable for the transfer.
|
||||
$node = Node::query()
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_listen', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate'])
|
||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemon_connect', 'nodes.memory', 'nodes.disk', 'nodes.cpu', 'nodes.memory_overallocate', 'nodes.disk_overallocate', 'nodes.cpu_overallocate'])
|
||||
->withSum('servers', 'disk')
|
||||
->withSum('servers', 'memory')
|
||||
->withSum('servers', 'cpu')
|
||||
|
@ -2,54 +2,20 @@
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Env;
|
||||
use RuntimeException;
|
||||
|
||||
trait EnvironmentWriterTrait
|
||||
{
|
||||
/**
|
||||
* Escapes an environment value by looking for any characters that could
|
||||
* reasonably cause environment parsing issues. Those values are then wrapped
|
||||
* in quotes before being returned.
|
||||
*/
|
||||
public function escapeEnvironmentValue(string $value): string
|
||||
{
|
||||
if (!preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) {
|
||||
return sprintf('"%s"', addcslashes($value, '\\"'));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the .env file for the application using the passed in values.
|
||||
*
|
||||
* @param array<string, mixed> $values
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function writeToEnvironment(array $values = []): void
|
||||
{
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
throw new Exception('Cannot locate .env file, was this software installed correctly?');
|
||||
}
|
||||
|
||||
$saveContents = file_get_contents($path);
|
||||
if ($saveContents === false) {
|
||||
$saveContents = '';
|
||||
}
|
||||
|
||||
collect($values)->each(function ($value, $key) use (&$saveContents) {
|
||||
$key = strtoupper($key);
|
||||
$saveValue = sprintf('%s=%s', $key, $this->escapeEnvironmentValue($value ?? ''));
|
||||
|
||||
if (preg_match_all('/^' . $key . '=(.*)$/m', $saveContents) < 1) {
|
||||
$saveContents = $saveContents . PHP_EOL . $saveValue;
|
||||
} else {
|
||||
$saveContents = preg_replace('/^' . $key . '=(.*)$/m', $saveValue, $saveContents);
|
||||
}
|
||||
});
|
||||
|
||||
file_put_contents($path, $saveContents);
|
||||
Env::writeVariables($values, base_path('.env'), true);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,20 @@ if (!function_exists('is_ip')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('is_ipv4')) {
|
||||
function is_ipv4(?string $address): bool
|
||||
{
|
||||
return $address !== null && filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('is_ipv6')) {
|
||||
function is_ipv6(?string $address): bool
|
||||
{
|
||||
return $address !== null && filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('convert_bytes_to_readable')) {
|
||||
function convert_bytes_to_readable(int $bytes, int $decimals = 2, ?int $base = null): string
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "pelican-dev/panel",
|
||||
"description": "The free, open-source game management panel. Supporting Minecraft, Spigot, BungeeCord, and SRCDS servers.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"require": {
|
||||
"php": "^8.2 || ^8.3 || ^8.4",
|
||||
"ext-intl": "*",
|
||||
@ -9,17 +10,18 @@
|
||||
"ext-pdo": "*",
|
||||
"ext-zip": "*",
|
||||
"abdelhamiderrahmouni/filament-monaco-editor": "^0.2.5",
|
||||
"aws/aws-sdk-php": "^3.342",
|
||||
"aws/aws-sdk-php": "^3.343",
|
||||
"aymanalhattami/filament-context-menu": "^1.0",
|
||||
"calebporzio/sushi": "^2.5",
|
||||
"chillerlan/php-qrcode": "^5.0.2",
|
||||
"dedoc/scramble": "^0.12.10",
|
||||
"doctrine/dbal": "~3.6.0",
|
||||
"filament/filament": "^3.3",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"laravel/framework": "^12.10",
|
||||
"laravel/framework": "^12.17",
|
||||
"laravel/helpers": "^1.7",
|
||||
"laravel/sanctum": "^4.0.2",
|
||||
"laravel/socialite": "^5.19",
|
||||
"laravel/sanctum": "^4.1",
|
||||
"laravel/socialite": "^5.21",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/ui": "^4.6",
|
||||
"lcobucci/jwt": "~4.3.0",
|
||||
@ -33,10 +35,10 @@
|
||||
"socialiteproviders/authentik": "^5.2",
|
||||
"socialiteproviders/discord": "^4.2",
|
||||
"socialiteproviders/steam": "^4.3",
|
||||
"spatie/laravel-data": "^4.14",
|
||||
"spatie/laravel-data": "^4.15",
|
||||
"spatie/laravel-fractal": "^6.3",
|
||||
"spatie/laravel-health": "^1.34",
|
||||
"spatie/laravel-permission": "^6.16",
|
||||
"spatie/laravel-permission": "^6.19",
|
||||
"spatie/laravel-query-builder": "^6.3",
|
||||
"spatie/temporary-directory": "^2.3",
|
||||
"symfony/http-client": "^7.2",
|
||||
@ -49,13 +51,15 @@
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^3.5",
|
||||
"fakerphp/faker": "^1.23.1",
|
||||
"larastan/larastan": "3.x-dev#5bd1c40edb43a727584081e74e9a1a2a201ea2ee",
|
||||
"larastan/larastan": "^3.4",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.15.3",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6.11",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"pestphp/pest": "^3.7",
|
||||
"pestphp/pest-plugin-faker": "^3.0",
|
||||
"pestphp/pest-plugin-livewire": "^3.0",
|
||||
"spatie/laravel-ignition": "^2.9"
|
||||
},
|
||||
"autoload": {
|
||||
|
1090
composer.lock
generated
1090
composer.lock
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user