mirror of
https://github.com/pelican-dev/panel.git
synced 2025-06-16 21:31:07 +02:00
Compare commits
No commits in common. "main" and "v1.0.0-beta18" have entirely different histories.
main
...
v1.0.0-bet
@ -3,4 +3,5 @@ APP_DEBUG=false
|
||||
APP_KEY=
|
||||
APP_URL=http://panel.test
|
||||
APP_INSTALLED=false
|
||||
APP_TIMEZONE=UTC
|
||||
APP_LOCALE=en
|
||||
|
15
.github/CODEOWNERS
vendored
Normal file
15
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Lines starting with '#' are comments.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
|
||||
# More details are here: https://help.github.com/articles/about-codeowners/
|
||||
|
||||
# The '*' pattern is global owners.
|
||||
|
||||
# Order is important. The last matching pattern has the most precedence.
|
||||
# The folders are ordered as follows:
|
||||
|
||||
# In each subsection folders are ordered first by depth, then alphabetically.
|
||||
# This should make it easy to add new rules without breaking existing ones.
|
||||
|
||||
# Global
|
||||
* @pelican-dev/panel
|
76
.github/workflows/ci.yaml
vendored
76
.github/workflows/ci.yaml
vendored
@ -213,79 +213,3 @@ jobs:
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration
|
||||
|
||||
postgresql:
|
||||
name: PostgreSQL
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4]
|
||||
database: ["postgres:14"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
env:
|
||||
POSTGRES_DB: testing
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
APP_ENV: testing
|
||||
APP_DEBUG: "false"
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
DB_CONNECTION: pgsql
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: postgres
|
||||
DB_PASSWORD: postgres
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration
|
||||
|
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 --error-format=github
|
||||
run: vendor/bin/phpstan --memory-limit=-1
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
@ -23,7 +24,8 @@ yarn-error.log
|
||||
/.vscode
|
||||
|
||||
public/assets/manifest.json
|
||||
/database/*.sqlite*
|
||||
/database/*.sqlite
|
||||
/database/*.sqlite-journal
|
||||
filament-monaco-editor/
|
||||
_ide_helper*
|
||||
/.phpstorm.meta.php
|
||||
|
26
Dockerfile
26
Dockerfile
@ -1,9 +1,16 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.13-labs
|
||||
# Pelican Production Dockerfile
|
||||
|
||||
##
|
||||
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
|
||||
##
|
||||
|
||||
# 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.3-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
|
||||
|
||||
# RUN rm /usr/local/bin/install-php-extensions
|
||||
|
||||
# ================================
|
||||
# Stage 1-1: Composer Install
|
||||
@ -75,16 +82,13 @@ 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
|
||||
# Symlink to env/database path, as www-data won't be able to write to webroot
|
||||
&& 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 \
|
||||
# Create necessary directories
|
||||
&& mkdir -p /pelican-data /var/run/supervisord /etc/supercronic \
|
||||
# Finally allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
|
||||
|
||||
# Configure Supervisor
|
||||
|
@ -1,10 +1,10 @@
|
||||
# ================================
|
||||
# Stage 0: Build PHP Base Image
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
|
||||
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 install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql
|
||||
|
||||
RUN rm /usr/local/bin/install-php-extensions
|
112
Dockerfile.dev
112
Dockerfile.dev
@ -1,112 +0,0 @@
|
||||
# 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" ]
|
@ -14,9 +14,9 @@ class NodeVersionsCheck extends Check
|
||||
|
||||
public function run(): Result
|
||||
{
|
||||
$all = Node::all();
|
||||
$all = Node::query()->count();
|
||||
|
||||
if ($all->isEmpty()) {
|
||||
if ($all === 0) {
|
||||
$result = Result::make()
|
||||
->notificationMessage(trans('admin/health.results.nodeversions.no_nodes_created'))
|
||||
->shortSummary(trans('admin/health.results.nodeversions.no_nodes'));
|
||||
@ -25,18 +25,16 @@ class NodeVersionsCheck extends Check
|
||||
return $result;
|
||||
}
|
||||
|
||||
$outdated = $all
|
||||
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && !$this->versionService->isLatestWings($node->systemInformation()['version']))
|
||||
->count();
|
||||
|
||||
$all = $all->count();
|
||||
$latestVersion = $this->versionService->latestWingsVersion();
|
||||
|
||||
$outdated = Node::query()->get()
|
||||
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && $node->systemInformation()['version'] !== $latestVersion)
|
||||
->count();
|
||||
|
||||
$result = Result::make()
|
||||
->meta([
|
||||
'all' => $all,
|
||||
'outdated' => $outdated,
|
||||
'latestVersion' => $latestVersion,
|
||||
])
|
||||
->shortSummary($outdated === 0 ? trans('admin/health.results.nodeversions.all_up_to_date') : trans('admin/health.results.nodeversions.outdated', ['outdated' => $outdated, 'all' => $all]));
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AppSettingsCommand extends Command
|
||||
{
|
||||
@ -20,13 +21,9 @@ class AppSettingsCommand extends Command
|
||||
|
||||
if (!config('app.key')) {
|
||||
$this->comment('Generating app key');
|
||||
$this->call('key:generate');
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
$this->comment('Creating storage link');
|
||||
$this->call('storage:link');
|
||||
|
||||
$this->comment('Caching components & icons');
|
||||
$this->call('filament:optimize');
|
||||
Artisan::call('filament:optimize');
|
||||
}
|
||||
}
|
||||
|
@ -18,17 +18,6 @@ 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';
|
||||
|
||||
|
@ -35,7 +35,7 @@ class RedisSetupCommand extends Command
|
||||
{
|
||||
$this->variables['CACHE_STORE'] = 'redis';
|
||||
$this->variables['QUEUE_CONNECTION'] = 'redis';
|
||||
$this->variables['SESSION_DRIVER'] = 'redis';
|
||||
$this->variables['SESSION_DRIVERS'] = 'redis';
|
||||
|
||||
$this->requestRedisSettings();
|
||||
|
||||
|
@ -6,7 +6,6 @@ use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
|
||||
use SplFileInfo;
|
||||
|
||||
class CleanServiceBackupFilesCommand extends Command
|
||||
{
|
||||
@ -33,10 +32,9 @@ class CleanServiceBackupFilesCommand extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
/** @var SplFileInfo[] */
|
||||
$files = $this->disk->files('services/.bak');
|
||||
|
||||
collect($files)->each(function ($file) {
|
||||
collect($files)->each(function (\SplFileInfo $file) {
|
||||
$lastModified = Carbon::createFromTimestamp($this->disk->lastModified($file->getPath()));
|
||||
if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) {
|
||||
$this->disk->delete($file->getPath());
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Console\Commands\Node;
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\Nodes\NodeCreationService;
|
||||
|
||||
class MakeNodeCommand extends Command
|
||||
{
|
||||
@ -24,13 +24,20 @@ 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.}';
|
||||
|
||||
protected $description = 'Creates a new node on the system via the CLI.';
|
||||
|
||||
/**
|
||||
* MakeNodeCommand constructor.
|
||||
*/
|
||||
public function __construct(private NodeCreationService $creationService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the command execution process.
|
||||
*
|
||||
@ -58,12 +65,11 @@ 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');
|
||||
|
||||
$node = Node::create($data);
|
||||
$node = $this->creationService->handle($data);
|
||||
$this->line(trans('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ 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;
|
||||
@ -30,11 +31,8 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
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();
|
||||
@ -43,6 +41,8 @@ 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();
|
||||
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasIcon;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum BackupStatus: string implements HasColor, HasIcon, HasLabel
|
||||
{
|
||||
case InProgress = 'in_progress';
|
||||
case Successful = 'successful';
|
||||
case Failed = 'failed';
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::InProgress => 'tabler-circle-dashed',
|
||||
self::Successful => 'tabler-circle-check',
|
||||
self::Failed => 'tabler-circle-x',
|
||||
};
|
||||
}
|
||||
|
||||
public function getColor(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::InProgress => 'primary',
|
||||
self::Successful => 'success',
|
||||
self::Failed => 'danger',
|
||||
};
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return str($this->value)->headline();
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ConsoleWidgetPosition: string
|
||||
{
|
||||
case Top = 'top';
|
||||
case AboveConsole = 'above_console';
|
||||
case BelowConsole = 'below_console';
|
||||
case Bottom = 'bottom';
|
||||
}
|
@ -62,7 +62,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
|
||||
self::Removing => 'warning',
|
||||
self::Missing => 'danger',
|
||||
self::Stopping => 'warning',
|
||||
self::Offline => 'danger',
|
||||
self::Offline => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
|
||||
|
||||
public function isStartable(): bool
|
||||
{
|
||||
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]);
|
||||
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
|
||||
}
|
||||
|
||||
public function isRestartable(): bool
|
||||
@ -97,16 +97,18 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
|
||||
return true;
|
||||
}
|
||||
|
||||
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
|
||||
return !in_array($this, [ContainerStatus::Offline]);
|
||||
}
|
||||
|
||||
public function isStoppable(): bool
|
||||
{
|
||||
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]);
|
||||
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]);
|
||||
}
|
||||
|
||||
public function isKillable(): bool
|
||||
{
|
||||
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]);
|
||||
// [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created]
|
||||
|
||||
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
|
||||
}
|
||||
}
|
||||
|
@ -14,24 +14,4 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@ -27,16 +27,8 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
|
||||
};
|
||||
}
|
||||
|
||||
public function getColor(bool $hex = false): string
|
||||
public function getColor(): string
|
||||
{
|
||||
if ($hex) {
|
||||
return match ($this) {
|
||||
self::Normal, self::Installing, self::RestoringBackup => '#2563EB',
|
||||
self::Suspended => '#D97706',
|
||||
self::InstallFailed, self::ReinstallFailed => '#EF4444',
|
||||
};
|
||||
}
|
||||
|
||||
return match ($this) {
|
||||
self::Normal => 'primary',
|
||||
self::Installing => 'primary',
|
||||
|
11
app/Events/Auth/DirectLogin.php
Normal file
11
app/Events/Auth/DirectLogin.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?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) {}
|
||||
}
|
16
app/Events/Auth/FailedPasswordReset.php
Normal file
16
app/Events/Auth/FailedPasswordReset.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?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) {}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Repository;
|
||||
|
||||
use Exception;
|
||||
|
||||
class FileNotEditableException extends Exception {}
|
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
abstract class AvatarProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
public static function getProvider(string $id): ?self
|
||||
{
|
||||
return Arr::get(static::$providers, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, static>
|
||||
*/
|
||||
public static function getAll(): array
|
||||
{
|
||||
return static::$providers;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
abstract public function get(User $user): ?string;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return Str::title($this->getId());
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar\Providers;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Models\User;
|
||||
|
||||
class GravatarProvider extends AvatarProvider
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gravatar';
|
||||
}
|
||||
|
||||
public function get(User $user): string
|
||||
{
|
||||
return 'https://gravatar.com/avatar/' . md5($user->email);
|
||||
}
|
||||
|
||||
public static function register(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Avatar\Providers;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Models\User;
|
||||
|
||||
class UiAvatarsProvider extends AvatarProvider
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'uiavatars';
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'UI Avatars';
|
||||
}
|
||||
|
||||
public function get(User $user): ?string
|
||||
{
|
||||
// UI Avatars is the default of filament so just return null here
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function register(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
35
app/Extensions/DynamicDatabaseConnection.php
Normal file
35
app/Extensions/DynamicDatabaseConnection.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions;
|
||||
|
||||
use App\Models\DatabaseHost;
|
||||
|
||||
class DynamicDatabaseConnection
|
||||
{
|
||||
public const DB_CHARSET = 'utf8';
|
||||
|
||||
public const DB_COLLATION = 'utf8_unicode_ci';
|
||||
|
||||
public const DB_DRIVER = 'mysql';
|
||||
|
||||
/**
|
||||
* Adds a dynamic database connection entry to the runtime config.
|
||||
*/
|
||||
public function set(string $connection, DatabaseHost|int $host, string $database = 'mysql'): void
|
||||
{
|
||||
if (!$host instanceof DatabaseHost) {
|
||||
$host = DatabaseHost::query()->findOrFail($host);
|
||||
}
|
||||
|
||||
config()->set('database.connections.' . $connection, [
|
||||
'driver' => self::DB_DRIVER,
|
||||
'host' => $host->host,
|
||||
'port' => $host->port,
|
||||
'database' => $database,
|
||||
'username' => $host->username,
|
||||
'password' => $host->password,
|
||||
'charset' => self::DB_CHARSET,
|
||||
'collation' => self::DB_COLLATION,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
abstract class FeatureProvider
|
||||
{
|
||||
/**
|
||||
* @var array<string, static>
|
||||
*/
|
||||
protected static array $providers = [];
|
||||
|
||||
/**
|
||||
* @param string[] $id
|
||||
* @return self|static[]
|
||||
*/
|
||||
public static function getProviders(string|array|null $id = null): array|self
|
||||
{
|
||||
if (is_array($id)) {
|
||||
return array_intersect_key(static::$providers, array_flip($id));
|
||||
}
|
||||
|
||||
return $id ? static::$providers[$id] : static::$providers;
|
||||
}
|
||||
|
||||
protected function __construct(protected Application $app)
|
||||
{
|
||||
if (array_key_exists($this->getId(), static::$providers)) {
|
||||
if (!$this->app->runningUnitTests()) {
|
||||
logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static::$providers[$this->getId()] = $this;
|
||||
}
|
||||
|
||||
abstract public function getId(): string;
|
||||
|
||||
/**
|
||||
* A matching subset string (case-insensitive) from the console output
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
abstract public function getListeners(): array;
|
||||
|
||||
abstract public function getAction(): Action;
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
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
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'(gsl token expired)',
|
||||
'(account not found)',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gsl_token';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
/** @var ServerVariable $serverVariable */
|
||||
$serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
|
||||
$query->where('env_variable', 'STEAM_ACC');
|
||||
})->first();
|
||||
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Invalid GSL token')
|
||||
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
|
||||
->modalSubmitActionLabel('Update GSL Token')
|
||||
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
|
||||
->form([
|
||||
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([
|
||||
fn (): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $serverVariable->variable->rules,
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
])
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn () => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn () => implode('|', $serverVariable->variable->rules))
|
||||
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
|
||||
])
|
||||
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
try {
|
||||
$new = $data['gsltoken'] ?? '';
|
||||
$original = $serverVariable->variable_value;
|
||||
|
||||
$serverVariable->update([
|
||||
'variable_value' => $new,
|
||||
]);
|
||||
|
||||
if ($original !== $new) {
|
||||
|
||||
Activity::event('server:startup.edit')
|
||||
->property([
|
||||
'variable' => $serverVariable->variable->env_variable,
|
||||
'old' => $original,
|
||||
'new' => $new,
|
||||
])
|
||||
->log();
|
||||
}
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('GSL Token updated')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Could not update GSL Token')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
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;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
class JavaVersion extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'java.lang.UnsupportedClassVersionError',
|
||||
'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',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'java_version';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Unsupported Java Version')
|
||||
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
|
||||
->modalSubmitActionLabel('Update Docker Image')
|
||||
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
|
||||
->form([
|
||||
Placeholder::make('java')
|
||||
->label('Please select a supported version from the list below to continue starting the server.'),
|
||||
Select::make('image')
|
||||
->label('Docker Image')
|
||||
->disabled(fn () => !in_array($server->image, $server->egg->docker_images))
|
||||
->options(fn () => collect($server->egg->docker_images)->mapWithKeys(fn ($key, $value) => [$key => $value]))
|
||||
->selectablePlaceholder(false)
|
||||
->default(fn () => $server->image)
|
||||
->notIn(fn () => $server->image)
|
||||
->required()
|
||||
->preload()
|
||||
->native(false),
|
||||
])
|
||||
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
|
||||
try {
|
||||
$new = $data['image'];
|
||||
$original = $server->image;
|
||||
$server->forceFill(['image' => $new])->saveOrFail();
|
||||
|
||||
if ($original !== $server->image) {
|
||||
Activity::event('server:startup.image')
|
||||
->property(['old' => $original, 'new' => $new])
|
||||
->log();
|
||||
}
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('Docker image updated')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Could not update docker image')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class MinecraftEula extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'you need to agree to the eula in order to run the server',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'eula';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
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>.')))
|
||||
->modalSubmitActionLabel('I Accept')
|
||||
->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
|
||||
try {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
|
||||
|
||||
$powerRepository->setServer($server)->send('restart');
|
||||
|
||||
Notification::make()
|
||||
->title('Minecraft EULA accepted')
|
||||
->body('Server will restart now.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Could not accept Minecraft EULA')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class PIDLimit extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'pthread_create failed',
|
||||
'failed to create thread',
|
||||
'unable to create thread',
|
||||
'unable to create native thread',
|
||||
'unable to create new native thread',
|
||||
'exception in thread "craft async scheduler management thread"',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'pid_limit';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->icon('tabler-alert-triangle')
|
||||
->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
|
||||
->modalDescription(new HtmlString(Blade::render(
|
||||
auth()->user()->isAdmin() ? <<<'HTML'
|
||||
<p>
|
||||
This server has reached the maximum process or memory limit.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
Increasing <code>container_pid_limit</code> in the wings
|
||||
configuration, <code>config.yml</code>, might help resolve
|
||||
this issue.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
<b>Note: Wings must be restarted for the configuration file changes to take effect</b>
|
||||
</p>
|
||||
HTML
|
||||
:
|
||||
<<<'HTML'
|
||||
<p>
|
||||
This server is attempting to use more resources than allocated. Please contact the administrator
|
||||
and give them the error below.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
<code>
|
||||
pthread_create failed, Possibly out of memory or process/resource limits reached
|
||||
</code>
|
||||
</p>
|
||||
HTML
|
||||
)))
|
||||
->modalCancelActionLabel('Close')
|
||||
->action(fn () => null);
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Features;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class SteamDiskSpace extends FeatureProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
/** @return array<string> */
|
||||
public function getListeners(): array
|
||||
{
|
||||
return [
|
||||
'steamcmd needs 250mb of free disk space to update',
|
||||
'0x202 after update job',
|
||||
];
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'steam_disk_space';
|
||||
}
|
||||
|
||||
public function getAction(): Action
|
||||
{
|
||||
return Action::make($this->getId())
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Out of available disk space...')
|
||||
->modalDescription(new HtmlString(Blade::render(
|
||||
auth()->user()->isAdmin() ? <<<'HTML'
|
||||
<p>
|
||||
This server has run out of available disk space and cannot complete the install or update
|
||||
process.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
Ensure the machine has enough disk space by typing{' '}
|
||||
<code class="rounded py-1 px-2">df -h</code> on the machine hosting
|
||||
this server. Delete files or increase the available disk space to resolve the issue.
|
||||
</p>
|
||||
HTML
|
||||
:
|
||||
<<<'HTML'
|
||||
<p>
|
||||
This server has run out of available disk space and cannot complete the install or update
|
||||
process. Please get in touch with the administrator(s) and inform them of disk space issues.
|
||||
</p>
|
||||
HTML
|
||||
)))
|
||||
->modalCancelActionLabel('Close')
|
||||
->action(fn () => null);
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -6,8 +6,8 @@ use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
use SocialiteProviders\Discord\Provider;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
@ -34,15 +34,15 @@ final class DiscordProvider extends OAuthProvider
|
||||
Step::make('Register new Discord OAuth App')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))),
|
||||
->content(new HtmlString('<p>Visit the <u><a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a></u> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b>, you will need them in the final step.</p>')),
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
|
||||
TextInput::make('_noenv_callback')
|
||||
->label('Redirect URL')
|
||||
->dehydrated()
|
||||
->disabled()
|
||||
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
|
||||
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
|
||||
->formatStateUsing(fn () => config('app.url') . (Str::endsWith(config('app.url'), '/') ? '' : '/') . 'auth/oauth/callback/discord'),
|
||||
]),
|
||||
], parent::getSetupSteps());
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
final class GithubProvider extends OAuthProvider
|
||||
@ -28,13 +28,13 @@ final class GithubProvider extends OAuthProvider
|
||||
Step::make('Register new Github OAuth App')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
|
||||
->content(new HtmlString('<p>Visit the <u><a href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</a></u>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>')),
|
||||
TextInput::make('_noenv_callback')
|
||||
->label('Authorization callback URL')
|
||||
->dehydrated()
|
||||
->disabled()
|
||||
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->default(fn () => url('/auth/oauth/callback/github')),
|
||||
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
|
||||
->default(fn () => config('app.url') . (Str::endsWith(config('app.url'), '/') ? '' : '/') . 'auth/oauth/callback/github'),
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
|
||||
]),
|
||||
|
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\OAuth\Providers;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
final class GitlabProvider extends OAuthProvider
|
||||
{
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return 'gitlab';
|
||||
}
|
||||
|
||||
public function getServiceConfig(): array
|
||||
{
|
||||
return array_merge(parent::getServiceConfig(), [
|
||||
'host' => env('OAUTH_GITLAB_HOST'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSettingsForm(): array
|
||||
{
|
||||
return array_merge(parent::getSettingsForm(), [
|
||||
TextInput::make('OAUTH_GITLAB_HOST')
|
||||
->label('Custom Host')
|
||||
->placeholder('Only set a custom host if you are self hosting gitlab')
|
||||
->columnSpan(2)
|
||||
->url()
|
||||
->autocomplete(false)
|
||||
->default(env('OAUTH_GITLAB_HOST')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSetupSteps(): array
|
||||
{
|
||||
return array_merge([
|
||||
Step::make('Register new Gitlab OAuth App')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(Blade::render('Check out the <x-filament::link href="https://docs.gitlab.com/integration/oauth_provider/" target="_blank">Gitlab docs</x-filament::link> on how to create the oauth app.'))),
|
||||
TextInput::make('_noenv_callback')
|
||||
->label('Redirect URI')
|
||||
->dehydrated()
|
||||
->disabled()
|
||||
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->default(fn () => url('/auth/oauth/callback/gitlab')),
|
||||
]),
|
||||
], parent::getSetupSteps());
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'tabler-brand-gitlab';
|
||||
}
|
||||
|
||||
public function getHexColor(): string
|
||||
{
|
||||
return '#fca326';
|
||||
}
|
||||
|
||||
public static function register(Application $app): self
|
||||
{
|
||||
return new self($app);
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use SocialiteProviders\Steam\Provider;
|
||||
|
||||
@ -59,7 +58,7 @@ final class SteamProvider extends OAuthProvider
|
||||
Step::make('Create API Key')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(Blade::render('Visit <x-filament::link href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</x-filament::link> to generate an API key.'))),
|
||||
->content(new HtmlString('Visit <u><a href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</a></u> to generate an API key.')),
|
||||
]),
|
||||
], parent::getSetupSteps());
|
||||
}
|
||||
|
@ -2,13 +2,36 @@
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
|
||||
use App\Filament\Admin\Resources\NodeResource\Pages\ListNodes;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Filament\Pages\Dashboard as BaseDashboard;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Dashboard extends BaseDashboard
|
||||
class Dashboard extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-layout-dashboard';
|
||||
|
||||
protected static string $view = 'filament.pages.dashboard';
|
||||
|
||||
protected ?string $heading = '';
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('admin/dashboard.title');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('admin/dashboard.title');
|
||||
}
|
||||
|
||||
protected static ?string $slug = '/';
|
||||
|
||||
private SoftwareVersionService $softwareVersionService;
|
||||
|
||||
public function mount(SoftwareVersionService $softwareVersionService): void
|
||||
@ -16,18 +39,51 @@ class Dashboard extends BaseDashboard
|
||||
$this->softwareVersionService = $softwareVersionService;
|
||||
}
|
||||
|
||||
public function getColumns(): int
|
||||
public function getViewData(): array
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
return [
|
||||
'inDevelopment' => config('app.version') === 'canary',
|
||||
'version' => $this->softwareVersionService->currentPanelVersion(),
|
||||
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
|
||||
'isLatest' => $this->softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
'serversCount' => Server::query()->count(),
|
||||
'usersCount' => User::query()->count(),
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return trans('admin/dashboard.heading');
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
{
|
||||
return trans('admin/dashboard.version', ['version' => $this->softwareVersionService->currentPanelVersion()]);
|
||||
'devActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
|
||||
->icon('tabler-brand-github')
|
||||
->url('https://github.com/pelican-dev/panel/issues', true),
|
||||
],
|
||||
'updateActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-update-available.heading'))
|
||||
->icon('tabler-clipboard-text')
|
||||
->url('https://pelican.dev/docs/panel/update', true)
|
||||
->color('warning'),
|
||||
],
|
||||
'nodeActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-first-node.button_label'))
|
||||
->icon('tabler-server-2')
|
||||
->url(CreateNode::getUrl()),
|
||||
],
|
||||
'supportActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-help.button_docs'))
|
||||
->icon('tabler-speedboat')
|
||||
->url('https://pelican.dev/docs', true),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Extensions\Avatar\AvatarProvider;
|
||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
|
||||
use App\Extensions\OAuth\Providers\OAuthProvider;
|
||||
use App\Models\Backup;
|
||||
@ -13,7 +12,6 @@ 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;
|
||||
@ -34,7 +32,6 @@ 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;
|
||||
@ -120,26 +117,12 @@ class Settings extends Page implements HasForms
|
||||
->label(trans('admin/setting.general.app_name'))
|
||||
->required()
|
||||
->default(env('APP_NAME', 'Pelican')),
|
||||
Group::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
TextInput::make('APP_LOGO')
|
||||
->label(trans('admin/setting.general.app_logo'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('admin/setting.general.app_logo_help'))
|
||||
->default(env('APP_LOGO'))
|
||||
->placeholder('/pelican.svg'),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label(trans('admin/setting.general.app_favicon'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('admin/setting.general.app_favicon_help'))
|
||||
->required()
|
||||
->default(env('APP_FAVICON', '/pelican.ico'))
|
||||
->placeholder('/pelican.ico'),
|
||||
]),
|
||||
Group::make()
|
||||
->columns(2)
|
||||
->schema([
|
||||
->default(env('APP_FAVICON', '/pelican.ico')),
|
||||
Toggle::make('APP_DEBUG')
|
||||
->label(trans('admin/setting.general.debug_mode'))
|
||||
->inline(false)
|
||||
@ -160,27 +143,6 @@ 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)
|
||||
->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()]))
|
||||
->selectablePlaceholder(false)
|
||||
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
|
||||
Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
|
||||
->label(trans('admin/setting.general.uploadable_avatars'))
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->formatStateUsing(fn ($state) => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_UPLOADABLE_AVATARS', (bool) $state))
|
||||
->default(env('FILAMENT_UPLOADABLE_AVATARS', config('panel.filament.uploadable-avatars'))),
|
||||
]),
|
||||
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
|
||||
->label(trans('admin/setting.general.unit_prefix'))
|
||||
->inline()
|
||||
@ -202,18 +164,12 @@ 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(',', Arr::wrap(config('trustedproxy.proxies')))))
|
||||
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
|
||||
->hintActions([
|
||||
FormAction::make('clear')
|
||||
->label(trans('admin/setting.general.clear'))
|
||||
@ -248,6 +204,12 @@ 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'))),
|
||||
];
|
||||
}
|
||||
|
||||
@ -335,7 +297,7 @@ class Settings extends Page implements HasForms
|
||||
'mail.mailers.smtp.port' => config('mail.mailers.smtp.port'),
|
||||
'mail.mailers.smtp.username' => config('mail.mailers.smtp.username'),
|
||||
'mail.mailers.smtp.password' => config('mail.mailers.smtp.password'),
|
||||
'mail.mailers.smtp.scheme' => config('mail.mailers.smtp.scheme'),
|
||||
'mail.mailers.smtp.encryption' => config('mail.mailers.smtp.encryption'),
|
||||
'mail.from.address' => config('mail.from.address'),
|
||||
'mail.from.name' => config('mail.from.name'),
|
||||
'services.mailgun.domain' => config('services.mailgun.domain'),
|
||||
@ -351,7 +313,7 @@ class Settings extends Page implements HasForms
|
||||
'mail.mailers.smtp.port' => $get('MAIL_PORT'),
|
||||
'mail.mailers.smtp.username' => $get('MAIL_USERNAME'),
|
||||
'mail.mailers.smtp.password' => $get('MAIL_PASSWORD'),
|
||||
'mail.mailers.smtp.scheme' => $get('MAIL_SCHEME'),
|
||||
'mail.mailers.smtp.encryption' => $get('MAIL_SCHEME'),
|
||||
'mail.from.address' => $get('MAIL_FROM_ADDRESS'),
|
||||
'mail.from.name' => $get('MAIL_FROM_NAME'),
|
||||
'services.mailgun.domain' => $get('MAILGUN_DOMAIN'),
|
||||
@ -415,16 +377,22 @@ class Settings extends Page implements HasForms
|
||||
->revealable()
|
||||
->default(env('MAIL_PASSWORD')),
|
||||
ToggleButtons::make('MAIL_SCHEME')
|
||||
->label(trans('admin/setting.mail.smtp.scheme'))
|
||||
->label(trans('admin/setting.mail.smtp.encryption'))
|
||||
->inline()
|
||||
->options([
|
||||
'smtp' => 'SMTP',
|
||||
'smtps' => 'SMTPS',
|
||||
'tls' => trans('admin/setting.mail.smtp.tls'),
|
||||
'ssl' => trans('admin/setting.mail.smtp.ssl'),
|
||||
'' => trans('admin/setting.mail.smtp.none'),
|
||||
])
|
||||
->default(env('MAIL_SCHEME', config('mail.mailers.smtp.scheme')))
|
||||
->default(env('MAIL_SCHEME', config('mail.mailers.smtp.encryption', 'tls')))
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$set('MAIL_PORT', $state === 'smtps' ? 587 : 2525);
|
||||
$port = match ($state) {
|
||||
'tls' => 587,
|
||||
'ssl' => 465,
|
||||
default => 25,
|
||||
};
|
||||
$set('MAIL_PORT', $port);
|
||||
}),
|
||||
]),
|
||||
Section::make(trans('admin/setting.mail.mailgun.mailgun_title'))
|
||||
@ -630,6 +598,7 @@ 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'))),
|
||||
@ -640,6 +609,7 @@ 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'))),
|
||||
@ -727,17 +697,10 @@ class Settings extends Page implements HasForms
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpan(1)
|
||||
->columnSpanFull()
|
||||
->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'))
|
||||
@ -766,7 +729,6 @@ 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', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
|
||||
->url(fn (ApiKey $apiKey) => auth()->user()->can('update user', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
|
@ -16,7 +16,6 @@ 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
|
||||
{
|
||||
@ -28,7 +27,7 @@ class DatabaseHostResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
@ -145,7 +144,7 @@ class DatabaseHostResource extends Resource
|
||||
->preload()
|
||||
->helperText(trans('admin/databasehost.linked_nodes_help'))
|
||||
->label(trans('admin/databasehost.linked_nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
|
||||
->relationship('nodes', 'name'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@ -159,15 +158,4 @@ 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,30 +4,14 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\DatabaseHostResource;
|
||||
use App\Services\Databases\Hosts\HostCreationService;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
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;
|
||||
use PDOException;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
use HasWizard;
|
||||
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
@ -39,118 +23,18 @@ class CreateDatabaseHost extends CreateRecord
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
/** @return Step[] */
|
||||
public function getSteps(): array
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Step::make(trans('admin/databasehost.setup.preparations'))
|
||||
->columns()
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(trans('admin/databasehost.setup.note')),
|
||||
Toggle::make('different_server')
|
||||
->label(new HtmlString(trans('admin/databasehost.setup.different_server')))
|
||||
->dehydrated(false)
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn ($state, Set $set) => $state ? $set('panel_ip', gethostbyname(str(config('app.url'))->replace(['http:', 'https:', '/'], ''))) : '127.0.0.1'),
|
||||
Hidden::make('panel_ip')
|
||||
->default('127.0.0.1')
|
||||
->dehydrated(false),
|
||||
TextInput::make('username')
|
||||
->label(trans('admin/databasehost.username'))
|
||||
->helperText(trans('admin/databasehost.username_help'))
|
||||
->required()
|
||||
->default('pelicanuser')
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->label(trans('admin/databasehost.password'))
|
||||
->helperText(trans('admin/databasehost.password_help'))
|
||||
->required()
|
||||
->default(Str::password(16))
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255),
|
||||
])
|
||||
->afterValidation(function (Get $get, Set $set) {
|
||||
$set('create_user', "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';");
|
||||
$set('assign_permissions', "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;");
|
||||
}),
|
||||
Step::make(trans('admin/databasehost.setup.database_setup'))
|
||||
->schema([
|
||||
Fieldset::make(trans('admin/databasehost.setup.database_user'))
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(trans('admin/databasehost.setup.cli_login')))
|
||||
->columnSpanFull(),
|
||||
TextInput::make('create_user')
|
||||
->label(trans('admin/databasehost.setup.command_create_user'))
|
||||
->default(fn (Get $get) => "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';")
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->columnSpanFull(),
|
||||
TextInput::make('assign_permissions')
|
||||
->label(trans('admin/databasehost.setup.command_assign_permissions'))
|
||||
->default(fn (Get $get) => "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;")
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
->columnSpanFull(),
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(trans('admin/databasehost.setup.cli_exit')))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Fieldset::make(trans('admin/databasehost.setup.external_access'))
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString(trans('admin/databasehost.setup.allow_external_access')))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Step::make(trans('admin/databasehost.setup.panel_setup'))
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->label(trans('admin/databasehost.host'))
|
||||
->helperText(trans('admin/databasehost.host_help'))
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('name', $state))
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->label(trans('admin/databasehost.port'))
|
||||
->helperText(trans('admin/databasehost.port_help'))
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
TextInput::make('max_databases')
|
||||
->label(trans('admin/databasehost.max_database'))
|
||||
->helpertext(trans('admin/databasehost.max_databases_help'))
|
||||
->placeholder(trans('admin/databasehost.unlimited'))
|
||||
->numeric(),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/databasehost.display_name'))
|
||||
->helperText(trans('admin/databasehost.display_name_help'))
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Select::make('node_ids')
|
||||
->multiple()
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText(trans('admin/databasehost.linked_nodes_help'))
|
||||
->label(trans('admin/databasehost.linked_nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
|
||||
]),
|
||||
$this->getCreateFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
try {
|
||||
|
@ -71,10 +71,10 @@ class DatabasesRelationManager extends RelationManager
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete', $database)),
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
|
||||
ViewAction::make()
|
||||
->color('primary')
|
||||
->hidden(fn () => !auth()->user()->can('viewAny', Database::class)),
|
||||
->hidden(fn () => !auth()->user()->can('viewList database')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class EggResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
|
||||
return trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
|
@ -243,7 +243,6 @@ class CreateEgg extends CreateRecord
|
||||
->default('ghcr.io/pelican-eggs/installers:debian'),
|
||||
Select::make('script_entry')
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->default('bash')
|
||||
->options(['bash', 'ash', '/bin/bash'])
|
||||
|
@ -235,7 +235,6 @@ class EditEgg extends EditRecord
|
||||
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
|
||||
Select::make('script_entry')
|
||||
->label(trans('admin/egg.script_entry'))
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->options(['bash', 'ash', '/bin/bash'])
|
||||
->required(),
|
||||
|
@ -7,8 +7,6 @@ use App\Filament\Components\Actions\ImportEggAction as ImportEggHeaderAction;
|
||||
use App\Filament\Components\Tables\Actions\ExportEggAction;
|
||||
use App\Filament\Components\Tables\Actions\ImportEggAction;
|
||||
use App\Filament\Components\Tables\Actions\UpdateEggAction;
|
||||
use App\Filament\Components\Tables\Actions\UpdateEggBulkAction;
|
||||
use App\Filament\Components\Tables\Filters\TagsFilter;
|
||||
use App\Models\Egg;
|
||||
use Filament\Actions\CreateAction as CreateHeaderAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
@ -18,7 +16,6 @@ use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ReplicateAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ListEggs extends ListRecords
|
||||
@ -30,6 +27,7 @@ class ListEggs extends ListRecords
|
||||
return $table
|
||||
->searchable(true)
|
||||
->defaultPaginationPageOption(25)
|
||||
->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0)
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label('Id')
|
||||
@ -70,16 +68,7 @@ class ListEggs extends ListRecords
|
||||
->successRedirectUrl(fn (Egg $replica) => EditEgg::getUrl(['record' => $replica])),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DeleteBulkAction::make()
|
||||
->before(fn (DeleteBulkAction $action, Collection $records) => $action->records($records->filter(function ($egg) {
|
||||
/** @var Egg $egg */
|
||||
return $egg->servers_count <= 0;
|
||||
}))),
|
||||
UpdateEggBulkAction::make()
|
||||
->before(fn (UpdateEggBulkAction $action, Collection $records) => $action->records($records->filter(function ($egg) {
|
||||
/** @var Egg $egg */
|
||||
return cache()->get("eggs.$egg->uuid.update", false);
|
||||
}))),
|
||||
DeleteBulkAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-eggs')
|
||||
->emptyStateDescription('')
|
||||
@ -88,10 +77,6 @@ class ListEggs extends ListRecords
|
||||
CreateAction::make(),
|
||||
ImportEggAction::make()
|
||||
->multiple(),
|
||||
])
|
||||
->filters([
|
||||
TagsFilter::make()
|
||||
->model(Egg::class),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,6 @@ 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
|
||||
{
|
||||
@ -45,7 +44,7 @@ class MountResource extends Resource
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
@ -76,7 +75,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.writable')),
|
||||
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writeable')),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
@ -148,7 +147,7 @@ class MountResource extends Resource
|
||||
->preload(),
|
||||
Select::make('nodes')->multiple()
|
||||
->label(trans('admin/mount.nodes'))
|
||||
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->relationship('nodes', 'name')
|
||||
->searchable(['name', 'fqdn'])
|
||||
->preload(),
|
||||
]),
|
||||
@ -171,15 +170,4 @@ 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,7 +6,6 @@ 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
|
||||
{
|
||||
@ -33,12 +32,12 @@ class NodeResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
|
||||
return trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return (string) static::getEloquentQuery()->count() ?: null;
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
@ -57,11 +56,4 @@ 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'));
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,9 @@
|
||||
namespace App\Filament\Admin\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
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;
|
||||
@ -46,8 +44,7 @@ class CreateNode extends CreateRecord
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rules(Node::getRulesForField('fqdn'))
|
||||
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
@ -124,10 +121,15 @@ class CreateNode extends CreateRecord
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
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'))
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('admin/node.port'))
|
||||
->helperText(trans('admin/node.port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
@ -145,15 +147,14 @@ class CreateNode extends CreateRecord
|
||||
->required()
|
||||
->maxLength(100),
|
||||
|
||||
Hidden::make('scheme')
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
|
||||
Hidden::make('behind_proxy')
|
||||
->default(false),
|
||||
|
||||
ToggleButtons::make('connection')
|
||||
ToggleButtons::make('scheme')
|
||||
->label(trans('admin/node.ssl'))
|
||||
->columnSpan(1)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
@ -166,43 +167,20 @@ class CreateNode extends CreateRecord
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
|
||||
->disableOptionWhen(fn (string $value): bool => $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')
|
||||
->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'),
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
]),
|
||||
Step::make('advanced')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
@ -418,13 +396,4 @@ class CreateNode extends CreateRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (!$data['behind_proxy']) {
|
||||
$data['daemon_listen'] = $data['daemon_connect'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
use App\Repositories\Daemon\DaemonConfigurationRepository;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use App\Services\Nodes\NodeAutoDeployService;
|
||||
use App\Services\Nodes\NodeUpdateService;
|
||||
@ -14,7 +13,6 @@ 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;
|
||||
@ -28,7 +26,7 @@ use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
@ -36,13 +34,12 @@ class EditNode extends EditRecord
|
||||
{
|
||||
protected static string $resource = NodeResource::class;
|
||||
|
||||
private DaemonConfigurationRepository $daemonConfigurationRepository;
|
||||
private bool $errored = false;
|
||||
|
||||
private NodeUpdateService $nodeUpdateService;
|
||||
|
||||
public function boot(DaemonConfigurationRepository $daemonConfigurationRepository, NodeUpdateService $nodeUpdateService): void
|
||||
public function boot(NodeUpdateService $nodeUpdateService): void
|
||||
{
|
||||
$this->daemonConfigurationRepository = $daemonConfigurationRepository;
|
||||
$this->nodeUpdateService = $nodeUpdateService;
|
||||
}
|
||||
|
||||
@ -111,8 +108,7 @@ class EditNode extends EditRecord
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rules(Node::getRulesForField('fqdn'))
|
||||
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
@ -181,10 +177,10 @@ class EditNode extends EditRecord
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan(1),
|
||||
TextInput::make('daemon_connect')
|
||||
TextInput::make('daemon_listen')
|
||||
->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'))
|
||||
->label(trans('admin/node.port'))
|
||||
->helperText(trans('admin/node.port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
@ -200,9 +196,7 @@ class EditNode extends EditRecord
|
||||
])
|
||||
->required()
|
||||
->maxLength(100),
|
||||
Hidden::make('scheme'),
|
||||
Hidden::make('behind_proxy'),
|
||||
ToggleButtons::make('connection')
|
||||
ToggleButtons::make('scheme')
|
||||
->label(trans('admin/node.ssl'))
|
||||
->columnSpan(1)
|
||||
->inline()
|
||||
@ -217,43 +211,20 @@ class EditNode extends EditRecord
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
|
||||
->disableOptionWhen(fn (string $value): bool => $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',
|
||||
])
|
||||
->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'),
|
||||
]),
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||
Tab::make('adv')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
->columns([
|
||||
@ -584,18 +555,7 @@ class EditNode extends EditRecord
|
||||
->modalHeading(trans('admin/node.reset_token'))
|
||||
->modalDescription(trans('admin/node.reset_help'))
|
||||
->action(function (Node $node) {
|
||||
try {
|
||||
$this->nodeUpdateService->handle($node, [], true);
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/node.error_connecting', ['node' => $node->name]))
|
||||
->body(trans('admin/node.error_connecting_description'))
|
||||
->color('warning')
|
||||
->icon('tabler-database')
|
||||
->warning()
|
||||
->send();
|
||||
|
||||
}
|
||||
Notification::make()->success()->title(trans('admin/node.token_reset'))->send();
|
||||
$this->fillForm();
|
||||
}),
|
||||
@ -625,6 +585,39 @@ class EditNode extends EditRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||
{
|
||||
if (!$record instanceof Node) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
try {
|
||||
$record = $this->nodeUpdateService->handle($record, $data);
|
||||
} catch (Exception $exception) {
|
||||
$this->errored = true;
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/node.error_connecting', ['node' => $record->name]))
|
||||
->body(trans('admin/node.error_connecting_description'))
|
||||
->color('warning')
|
||||
->icon('tabler-database')
|
||||
->warning()
|
||||
->send();
|
||||
|
||||
}
|
||||
|
||||
return parent::handleRecordUpdate($record, $data);
|
||||
}
|
||||
|
||||
protected function getSavedNotification(): ?Notification
|
||||
{
|
||||
if ($this->errored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parent::getSavedNotification();
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
@ -640,43 +633,9 @@ 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();
|
||||
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$changed = collect($node->getChanges())->except(['updated_at', 'name', 'tags', 'public', 'maintenance_mode', 'memory', 'memory_overallocate', 'disk', 'disk_overallocate', 'cpu', 'cpu_overallocate'])->all();
|
||||
|
||||
try {
|
||||
if ($changed) {
|
||||
$this->daemonConfigurationRepository->setNode($node)->update($node);
|
||||
}
|
||||
parent::getSavedNotification()?->send();
|
||||
} catch (ConnectionException) {
|
||||
Notification::make()
|
||||
->title(trans('admin/node.error_connecting', ['node' => $node->name]))
|
||||
->body(trans('admin/node.error_connecting_description'))
|
||||
->color('warning')
|
||||
->icon('tabler-database')
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getSavedNotification(): ?Notification
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getColumnSpan(): ?int
|
||||
|
@ -4,7 +4,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\NodeResource;
|
||||
use App\Filament\Components\Tables\Columns\NodeHealthColumn;
|
||||
use App\Filament\Components\Tables\Filters\TagsFilter;
|
||||
use App\Models\Node;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
@ -66,10 +65,6 @@ class ListNodes extends ListRecords
|
||||
->emptyStateHeading(trans('admin/node.no_nodes'))
|
||||
->emptyStateActions([
|
||||
CreateAction::make(),
|
||||
])
|
||||
->filters([
|
||||
TagsFilter::make()
|
||||
->model(Node::class),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,8 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
@ -31,12 +32,18 @@ class AllocationsRelationManager extends RelationManager
|
||||
public function setTitle(): string
|
||||
{
|
||||
return trans('admin/server.allocations');
|
||||
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('address')
|
||||
->recordTitleAttribute('ip')
|
||||
|
||||
// Non Primary Allocations
|
||||
// ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
|
||||
|
||||
// All assigned allocations
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||
->paginationPageOptions(['10', '20', '50', '100', '200', '500'])
|
||||
->searchable()
|
||||
@ -65,14 +72,14 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(trans('admin/node.table.ip')),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('create new allocation')
|
||||
Tables\Actions\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()
|
||||
->ip()
|
||||
->ipv4()
|
||||
->helperText(trans('admin/node.ip_help'))
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
->live()
|
||||
@ -89,15 +96,19 @@ 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)),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())),
|
||||
->authorize(fn () => auth()->user()->can('update node')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
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;
|
||||
@ -15,34 +16,22 @@ 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
|
||||
{
|
||||
$sessionKey = "node_stats.{$this->node->id}";
|
||||
$threads = $this->node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$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);
|
||||
$cpu = collect(cache()->get("nodes.{$this->node->id}.cpu_percent"))
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => Number::format($value * $threads, maxPrecision: 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($this->cpuHistory, 'cpu'),
|
||||
'data' => array_column($cpu, 'cpu'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
@ -50,7 +39,7 @@ class NodeCpuChart extends ChartWidget
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($this->cpuHistory, 'timestamp'),
|
||||
'labels' => array_column($cpu, 'timestamp'),
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
@ -80,10 +69,10 @@ class NodeCpuChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$data = array_slice(end($this->cpuHistory), -60);
|
||||
$threads = $this->node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language);
|
||||
$max = Number::format($this->threads * 100, locale: auth()->user()->language);
|
||||
$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);
|
||||
|
||||
return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
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;
|
||||
@ -15,36 +16,19 @@ 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
|
||||
{
|
||||
$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);
|
||||
$memUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->slice(-10)
|
||||
->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'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($this->memoryHistory, 'memory'),
|
||||
'data' => array_column($memUsed, 'memory'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
@ -52,7 +36,7 @@ class NodeMemoryChart extends ChartWidget
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($this->memoryHistory, 'timestamp'),
|
||||
'labels' => array_column($memUsed, 'timestamp'),
|
||||
'locale' => auth()->user()->language ?? 'en',
|
||||
];
|
||||
}
|
||||
@ -82,15 +66,16 @@ class NodeMemoryChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
$latestMemoryUsed = array_slice(end($this->memoryHistory), -60);
|
||||
$latestMemoryUsed = collect(cache()->get("nodes.{$this->node->id}.memory_used"))->last();
|
||||
$totalMemory = collect(cache()->get("nodes.{$this->node->id}.memory_total"))->last();
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
? 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';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? 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';
|
||||
? 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';
|
||||
|
||||
return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class NodeStorageChart extends ChartWidget
|
||||
{
|
||||
@ -45,8 +46,8 @@ class NodeStorageChart extends ChartWidget
|
||||
|
||||
$unused = $total - $used;
|
||||
|
||||
$used = round($used, 2);
|
||||
$unused = round($unused, 2);
|
||||
$used = Number::format($used, maxPrecision: 2);
|
||||
$unused = Number::format($unused, maxPrecision: 2);
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Filament\Admin\Resources;
|
||||
|
||||
use App\Enums\RolePermissionModels;
|
||||
use App\Enums\RolePermissionPrefixes;
|
||||
use App\Filament\Admin\Resources\RoleResource\Pages;
|
||||
use App\Models\Role;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
@ -10,7 +12,6 @@ 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;
|
||||
@ -49,7 +50,7 @@ class RoleResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
|
||||
return trans('admin/dashboard.user');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@ -70,11 +71,6 @@ 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')
|
||||
@ -99,16 +95,32 @@ class RoleResource extends Resource
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
$permissionSections = [];
|
||||
$permissions = [];
|
||||
|
||||
foreach (Role::getPermissionList() as $model => $permissions) {
|
||||
foreach (RolePermissionModels::cases() as $model) {
|
||||
$options = [];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
$options[$permission . ' ' . strtolower($model)] = Str::headline($permission);
|
||||
foreach (RolePermissionPrefixes::cases() as $prefix) {
|
||||
$options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix->value);
|
||||
}
|
||||
|
||||
$permissionSections[] = self::makeSection($model, $options);
|
||||
if (array_key_exists($model->value, Role::MODEL_SPECIFIC_PERMISSIONS)) {
|
||||
foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model->value] as $permission) {
|
||||
$options[$permission . ' ' . strtolower($model->value)] = Str::headline($permission);
|
||||
}
|
||||
}
|
||||
|
||||
$permissions[] = self::makeSection($model->value, $options);
|
||||
}
|
||||
|
||||
foreach (Role::SPECIAL_PERMISSIONS as $model => $prefixes) {
|
||||
$options = [];
|
||||
|
||||
foreach ($prefixes as $prefix) {
|
||||
$options[$prefix . ' ' . strtolower($model)] = Str::headline($prefix);
|
||||
}
|
||||
|
||||
$permissions[] = self::makeSection($model, $options);
|
||||
}
|
||||
|
||||
return $form
|
||||
@ -125,20 +137,12 @@ class RoleResource extends Resource
|
||||
->hidden(),
|
||||
Fieldset::make(trans('admin/role.permissions'))
|
||||
->columns(3)
|
||||
->schema($permissionSections)
|
||||
->schema($permissions)
|
||||
->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
Placeholder::make('permissions')
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -147,8 +151,6 @@ class RoleResource extends Resource
|
||||
*/
|
||||
private static function makeSection(string $model, array $options): Section
|
||||
{
|
||||
$model = ucwords($model);
|
||||
|
||||
$icon = null;
|
||||
|
||||
if (class_exists('\App\Filament\Admin\Resources\\' . $model . 'Resource')) {
|
||||
|
@ -3,12 +3,8 @@
|
||||
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
|
||||
{
|
||||
@ -35,35 +31,12 @@ class ServerResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
|
||||
return trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
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();
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
@ -74,11 +47,4 @@ 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,6 +15,7 @@ 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;
|
||||
@ -108,20 +109,14 @@ class CreateServer extends CreateRecord
|
||||
->disabledOn('edit')
|
||||
->prefixIcon('tabler-server-2')
|
||||
->selectablePlaceholder(false)
|
||||
->default(function () {
|
||||
/** @var ?Node $latestNode */
|
||||
$latestNode = auth()->user()->accessibleNodes()->latest()->first();
|
||||
$this->node = $latestNode;
|
||||
|
||||
return $this->node?->id;
|
||||
})
|
||||
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
])
|
||||
->live()
|
||||
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->relationship('node', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->afterStateUpdated(function (Set $set, $state) {
|
||||
@ -144,7 +139,6 @@ 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'))
|
||||
@ -189,7 +183,10 @@ class CreateServer extends CreateRecord
|
||||
$set('allocation_additional', null);
|
||||
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
|
||||
})
|
||||
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->placeholder(function (Get $get) {
|
||||
$node = Node::find($get('node_id'));
|
||||
|
||||
@ -206,7 +203,6 @@ 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;
|
||||
|
||||
@ -216,7 +212,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', []))
|
||||
->ip()
|
||||
->ipv4()
|
||||
->live()
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
@ -267,7 +263,10 @@ class CreateServer extends CreateRecord
|
||||
->columnSpan(2)
|
||||
->disabled(fn (Get $get) => $get('../../node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->placeholder(trans('admin/server.select_additional'))
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->relationship(
|
||||
@ -427,7 +426,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
Repeater::make('server_variables')
|
||||
->label('')
|
||||
->relationship('serverVariables', fn (Builder $query) => $query->orderByPowerJoins('variable.sort'))
|
||||
->relationship('serverVariables')
|
||||
->saveRelationshipsBeforeChildrenUsing(null)
|
||||
->saveRelationshipsUsing(null)
|
||||
->grid(2)
|
||||
@ -745,7 +744,7 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 4,
|
||||
])
|
||||
->columnSpan(6)
|
||||
->schema(fn (Get $get) => [
|
||||
->schema([
|
||||
Select::make('select_image')
|
||||
->label(trans('admin/server.image_name'))
|
||||
->live()
|
||||
@ -793,13 +792,19 @@ class CreateServer extends CreateRecord
|
||||
]),
|
||||
|
||||
KeyValue::make('docker_labels')
|
||||
->live()
|
||||
->label('Container Labels')
|
||||
->keyLabel(trans('admin/server.title'))
|
||||
->valueLabel(trans('admin/server.description'))
|
||||
->columnSpanFull(),
|
||||
|
||||
ServerResource::getMountCheckboxList($get),
|
||||
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(),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
|
@ -2,18 +2,16 @@
|
||||
|
||||
namespace App\Filament\Admin\Resources\ServerResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Enums\SuspendAction;
|
||||
use App\Filament\Admin\Resources\ServerResource;
|
||||
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
|
||||
use App\Filament\Components\Forms\Actions\PreviewStartupAction;
|
||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Models\User;
|
||||
@ -30,9 +28,8 @@ use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions as FormActions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
@ -51,12 +48,10 @@ 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\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use LogicException;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
@ -64,6 +59,8 @@ class EditServer extends EditRecord
|
||||
{
|
||||
protected static string $resource = ServerResource::class;
|
||||
|
||||
private bool $errored = false;
|
||||
|
||||
private DaemonServerRepository $daemonServerRepository;
|
||||
|
||||
public function boot(DaemonServerRepository $daemonServerRepository): void
|
||||
@ -136,39 +133,7 @@ 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'))
|
||||
@ -208,7 +173,7 @@ class EditServer extends EditRecord
|
||||
->maxLength(255),
|
||||
Select::make('node_id')
|
||||
->label(trans('admin/server.node'))
|
||||
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
|
||||
->relationship('node', 'name')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 1,
|
||||
@ -517,7 +482,6 @@ class EditServer extends EditRecord
|
||||
]),
|
||||
|
||||
KeyValue::make('docker_labels')
|
||||
->live()
|
||||
->label(trans('admin/server.container_labels'))
|
||||
->keyLabel(trans('admin/server.title'))
|
||||
->valueLabel(trans('admin/server.description'))
|
||||
@ -627,7 +591,7 @@ class EditServer extends EditRecord
|
||||
]);
|
||||
}
|
||||
|
||||
return $query->orderByPowerJoins('variable.sort');
|
||||
return $query;
|
||||
})
|
||||
->grid()
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||
@ -682,11 +646,17 @@ class EditServer extends EditRecord
|
||||
]),
|
||||
Tab::make(trans('admin/server.mounts'))
|
||||
->icon('tabler-layers-linked')
|
||||
->schema(fn (Get $get) => [
|
||||
ServerResource::getMountCheckboxList($get),
|
||||
->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(),
|
||||
]),
|
||||
Tab::make(trans('admin/server.databases'))
|
||||
->hidden(fn () => !auth()->user()->can('viewAny', Database::class))
|
||||
->hidden(fn () => !auth()->user()->can('viewList database'))
|
||||
->icon('tabler-database')
|
||||
->columns(4)
|
||||
->schema([
|
||||
@ -710,14 +680,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))
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database))
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->modalIcon('tabler-database-x')
|
||||
->modalHeading(trans('admin/server.delete_db_heading'))
|
||||
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
|
||||
->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')]))
|
||||
->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
|
||||
->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?')
|
||||
->action(function (DatabaseManagementService $databaseManagementService, $record) {
|
||||
$databaseManagementService->delete($record);
|
||||
$this->fillForm();
|
||||
@ -761,9 +731,9 @@ class EditServer extends EditRecord
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->columnSpan(4),
|
||||
FormActions::make([
|
||||
Forms\Components\Actions::make([
|
||||
Action::make('createDatabase')
|
||||
->authorize(fn () => auth()->user()->can('create', Database::class))
|
||||
->authorize(fn () => auth()->user()->can('create database'))
|
||||
->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')
|
||||
@ -829,50 +799,14 @@ class EditServer extends EditRecord
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
FormActions::make([
|
||||
Forms\Components\Actions::make([
|
||||
Action::make('toggleInstall')
|
||||
->label(trans('admin/server.toggle_install'))
|
||||
->disabled(fn (Server $server) => $server->isSuspended())
|
||||
->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->isFailedInstall()) {
|
||||
try {
|
||||
$reinstallService->handle($server);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.reinstall_started'))
|
||||
->success()
|
||||
->send();
|
||||
->action(function (ToggleInstallService $service, Server $server) {
|
||||
$service->handle($server);
|
||||
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.reinstall_failed'))
|
||||
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$toggleService->handle($server);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.install_toggled'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.install_toggle_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}),
|
||||
])->fullWidth(),
|
||||
ToggleButtons::make('')
|
||||
@ -881,7 +815,7 @@ class EditServer extends EditRecord
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
FormActions::make([
|
||||
Forms\Components\Actions::make([
|
||||
Action::make('toggleSuspend')
|
||||
->label(trans('admin/server.suspend'))
|
||||
->color('warning')
|
||||
@ -889,20 +823,12 @@ class EditServer extends EditRecord
|
||||
->action(function (SuspensionService $suspensionService, Server $server) {
|
||||
try {
|
||||
$suspensionService->handle($server, SuspendAction::Suspend);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/server.notifications.server_suspended'))
|
||||
->send();
|
||||
} catch (\Exception $exception) {
|
||||
Notification::make()->warning()->title(trans('admin/server.notifications.server_suspension'))->body($exception->getMessage())->send();
|
||||
}
|
||||
Notification::make()->success()->title(trans('admin/server.notifications.server_suspended'))->send();
|
||||
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->warning()
|
||||
->title(trans('admin/server.notifications.server_suspension'))
|
||||
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
Action::make('toggleUnsuspend')
|
||||
->label(trans('admin/server.unsuspend'))
|
||||
@ -911,20 +837,12 @@ class EditServer extends EditRecord
|
||||
->action(function (SuspensionService $suspensionService, Server $server) {
|
||||
try {
|
||||
$suspensionService->handle($server, SuspendAction::Unsuspend);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/server.notifications.server_unsuspended'))
|
||||
->send();
|
||||
} catch (\Exception $exception) {
|
||||
Notification::make()->warning()->title(trans('admin/server.notifications.server_suspension'))->body($exception->getMessage())->send();
|
||||
}
|
||||
Notification::make()->success()->title(trans('admin/server.notifications.server_unsuspended'))->send();
|
||||
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->warning()
|
||||
->title(trans('admin/server.notifications.server_suspension'))
|
||||
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
])->fullWidth(),
|
||||
ToggleButtons::make('')
|
||||
@ -937,36 +855,42 @@ class EditServer extends EditRecord
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
FormActions::make([
|
||||
Forms\Components\Actions::make([
|
||||
Action::make('transfer')
|
||||
->label(trans('admin/server.transfer'))
|
||||
->disabled(fn (Server $server) => Node::count() <= 1 || $server->isInConflictState())
|
||||
->modalheading(trans('admin/server.transfer'))
|
||||
->form($this->transferServer())
|
||||
->action(function (TransferServerService $transfer, Server $server, $data) {
|
||||
try {
|
||||
$transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', []));
|
||||
|
||||
Notification::make()
|
||||
->title('Transfer started')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Transfer failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
// ->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
|
||||
->disabled() //TODO!
|
||||
->form([ //TODO!
|
||||
Select::make('newNode')
|
||||
->label('New Node')
|
||||
->required()
|
||||
->options([
|
||||
true => 'on',
|
||||
false => 'off',
|
||||
]),
|
||||
Select::make('newMainAllocation')
|
||||
->label('New Main Allocation')
|
||||
->required()
|
||||
->options([
|
||||
true => 'on',
|
||||
false => 'off',
|
||||
]),
|
||||
Select::make('newAdditionalAllocation')
|
||||
->label('New Additional Allocations')
|
||||
->options([
|
||||
true => 'on',
|
||||
false => 'off',
|
||||
]),
|
||||
])
|
||||
->modalheading(trans('admin/server.transfer')),
|
||||
])->fullWidth(),
|
||||
ToggleButtons::make('')
|
||||
->hint(new HtmlString(trans('admin/server.transfer_help'))),
|
||||
->hint(trans('admin/server.transfer_help')),
|
||||
]),
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
FormActions::make([
|
||||
Forms\Components\Actions::make([
|
||||
Action::make('reinstall')
|
||||
->label(trans('admin/server.reinstall'))
|
||||
->color('danger')
|
||||
@ -974,24 +898,7 @@ class EditServer extends EditRecord
|
||||
->modalHeading(trans('admin/server.reinstall_modal_heading'))
|
||||
->modalDescription(trans('admin/server.reinstall_modal_description'))
|
||||
->disabled(fn (Server $server) => $server->isSuspended())
|
||||
->action(function (ReinstallServerService $service, Server $server) {
|
||||
try {
|
||||
$service->handle($server);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.reinstall_started'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.reinstall_failed'))
|
||||
->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
->action(fn (ReinstallServerService $service, Server $server) => $service->handle($server)),
|
||||
])->fullWidth(),
|
||||
ToggleButtons::make('')
|
||||
->hint(trans('admin/server.reinstall_help')),
|
||||
@ -1002,87 +909,33 @@ class EditServer extends EditRecord
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Component[] */
|
||||
protected function transferServer(): array
|
||||
protected function transferServer(Form $form): Form
|
||||
{
|
||||
return [
|
||||
Select::make('node_id')
|
||||
->label(trans('admin/server.node'))
|
||||
->prefixIcon('tabler-server-2')
|
||||
->selectablePlaceholder(false)
|
||||
->default(fn (Server $server) => Node::whereNot('id', $server->node->id)->first()?->id)
|
||||
->required()
|
||||
->live()
|
||||
->options(fn (Server $server) => Node::whereNot('id', $server->node->id)->pluck('name', 'id')->all()),
|
||||
Select::make('allocation_id')
|
||||
->label(trans('admin/server.primary_allocation'))
|
||||
->required()
|
||||
->prefixIcon('tabler-network')
|
||||
->disabled(fn (Get $get) => !$get('node_id'))
|
||||
->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address]))
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->placeholder(trans('admin/server.select_allocation')),
|
||||
Select::make('allocation_additional')
|
||||
->label(trans('admin/server.additional_allocations'))
|
||||
->multiple()
|
||||
->prefixIcon('tabler-network')
|
||||
->disabled(fn (Get $get) => !$get('node_id'))
|
||||
->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address]))
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->placeholder(trans('admin/server.select_additional')),
|
||||
];
|
||||
return $form
|
||||
->columns()
|
||||
->schema([
|
||||
Select::make('toNode')
|
||||
->label('New Node'),
|
||||
TextInput::make('newAllocation')
|
||||
->label('Allocation'),
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = $this->getRecord();
|
||||
|
||||
$canForceDelete = cache()->get("servers.$server->uuid.canForceDelete", false);
|
||||
|
||||
return [
|
||||
Actions\Action::make('Delete')
|
||||
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||
->color('danger')
|
||||
->label(trans('filament-actions::delete.single.label'))
|
||||
->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
|
||||
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
|
||||
->label(trans('filament-actions::delete.single.modal.actions.delete.label'))
|
||||
->requiresConfirmation()
|
||||
->action(function (Server $server, ServerDeletionService $service) {
|
||||
try {
|
||||
$service->handle($server);
|
||||
|
||||
return redirect(ListServers::getUrl(panel: 'admin'));
|
||||
} catch (ConnectionException) {
|
||||
cache()->put("servers.$server->uuid.canForceDelete", true, now()->addMinutes(5));
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.error_server_delete'))
|
||||
->body(trans('admin/server.notifications.error_server_delete_body'))
|
||||
->color('warning')
|
||||
->icon('tabler-database')
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
})
|
||||
->hidden(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)),
|
||||
Actions\Action::make('ForceDelete')
|
||||
->color('danger')
|
||||
->label(trans('filament-actions::force-delete.single.label'))
|
||||
->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
|
||||
->modalSubmitActionLabel(trans('filament-actions::force-delete.single.label'))
|
||||
->requiresConfirmation()
|
||||
->action(function (Server $server, ServerDeletionService $service) {
|
||||
try {
|
||||
$service->withForce()->handle($server);
|
||||
|
||||
return redirect(ListServers::getUrl(panel: 'admin'));
|
||||
} catch (ConnectionException) {
|
||||
cache()->forget("servers.$server->uuid.canForceDelete");
|
||||
}
|
||||
})
|
||||
->visible(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete', $server)),
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
Actions\Action::make('console')
|
||||
->label(trans('admin/server.console'))
|
||||
->icon('tabler-terminal')
|
||||
@ -1103,39 +956,46 @@ class EditServer extends EditRecord
|
||||
$data['description'] = '';
|
||||
}
|
||||
|
||||
unset($data['docker'], $data['status'], $data['allocation_id']);
|
||||
unset($data['docker'], $data['status']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = $this->record;
|
||||
if (!$record instanceof Server) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
$changed = collect($server->getChanges())->except(['updated_at', 'name', 'owner_id', 'condition', 'description', 'external_id', 'tags', 'cpu_pinning', 'allocation_limit', 'database_limit', 'backup_limit', 'skip_scripts'])->all();
|
||||
/** @var Server $record */
|
||||
$record = parent::handleRecordUpdate($record, $data);
|
||||
|
||||
try {
|
||||
if ($changed) {
|
||||
$this->daemonServerRepository->setServer($server)->sync();
|
||||
}
|
||||
parent::getSavedNotification()?->send();
|
||||
$this->daemonServerRepository->setServer($record)->sync();
|
||||
} catch (ConnectionException) {
|
||||
$this->errored = true;
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
->title(trans('admin/server.notifications.error_connecting', ['node' => $record->node->name]))
|
||||
->body(trans('admin/server.notifications.error_connecting_description'))
|
||||
->color('warning')
|
||||
->icon('tabler-database')
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
protected function getSavedNotification(): ?Notification
|
||||
{
|
||||
if ($this->errored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parent::getSavedNotification();
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
|
@ -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')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->hidden(!auth()->user()->can('update server'))
|
||||
->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')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
|
||||
->hidden(auth()->user()->can('update server'))
|
||||
->state(fn (Server $server) => $server->allocation->address),
|
||||
TextColumn::make('image')->hidden(),
|
||||
TextColumn::make('backups_count')
|
||||
|
@ -12,17 +12,14 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Filament\Tables;
|
||||
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;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
/**
|
||||
* @method Server getOwnerRecord()
|
||||
@ -35,18 +32,15 @@ class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
return $table
|
||||
->selectCurrentPageOnly()
|
||||
->recordTitleAttribute('address')
|
||||
->recordTitle(fn (Allocation $allocation) => $allocation->address)
|
||||
->recordTitleAttribute('ip')
|
||||
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
|
||||
->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',
|
||||
@ -62,11 +56,8 @@ 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())
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
|
||||
DissociateAction::make()
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : trans('admin/server.make_primary')),
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make()->label(trans('admin/server.create_allocation'))
|
||||
@ -76,8 +67,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->options(collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->label(trans('admin/server.ip_address'))
|
||||
->inlineLabel()
|
||||
->ip()
|
||||
->live()
|
||||
->ipv4()
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
@ -91,8 +81,9 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(trans('admin/server.ports'))
|
||||
->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(),
|
||||
])
|
||||
@ -105,21 +96,10 @@ class AllocationsRelationManager extends RelationManager
|
||||
->recordSelectSearchColumns(['ip', 'port'])
|
||||
->label(trans('admin/server.add_allocation')),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
DissociateBulkAction::make()
|
||||
->before(function (DissociateBulkAction $action, Collection $records) {
|
||||
$records = $records->filter(function ($allocation) {
|
||||
/** @var Allocation $allocation */
|
||||
return $allocation->id !== $this->getOwnerRecord()->allocation_id;
|
||||
});
|
||||
|
||||
if ($records->isEmpty()) {
|
||||
$action->failureNotificationTitle(trans('admin/server.notifications.dissociate_primary'))->failure();
|
||||
throw new Halt();
|
||||
}
|
||||
|
||||
return $records;
|
||||
}),
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DissociateBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ use App\Filament\Admin\Resources\UserResource\Pages;
|
||||
use App\Filament\Admin\Resources\UserResource\RelationManagers;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
@ -18,7 +17,6 @@ use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class UserResource extends Resource
|
||||
{
|
||||
@ -45,7 +43,7 @@ class UserResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.user');
|
||||
return trans('admin/dashboard.user');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@ -60,9 +58,8 @@ class UserResource extends Resource
|
||||
ImageColumn::make('picture')
|
||||
->visibleFrom('lg')
|
||||
->label('')
|
||||
->circular()
|
||||
->alignCenter()
|
||||
->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)),
|
||||
->extraImgAttributes(['class' => 'rounded-full'])
|
||||
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||
TextColumn::make('username')
|
||||
->label(trans('admin/user.username')),
|
||||
TextColumn::make('email')
|
||||
@ -123,26 +120,12 @@ class UserResource extends Resource
|
||||
->hintIconTooltip(fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
|
||||
->password(),
|
||||
CheckboxList::make('roles')
|
||||
->hidden(fn (User $user) => $user->isRootAdmin())
|
||||
->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
|
||||
->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
|
||||
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||
->relationship('roles', 'name')
|
||||
->dehydrated()
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
CheckboxList::make('root_admin_role')
|
||||
->visible(fn (User $user) => $user->isRootAdmin())
|
||||
->disabled()
|
||||
->options([
|
||||
'root_admin' => Role::ROOT_ADMIN,
|
||||
])
|
||||
->descriptions([
|
||||
'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
|
||||
])
|
||||
->formatStateUsing(fn () => ['root_admin'])
|
||||
->dehydrated(false)
|
||||
->label(trans('admin/user.admin_roles'))
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -33,13 +33,6 @@ class CreateUser extends CreateRecord
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function prepareForValidation($attributes): array
|
||||
{
|
||||
$attributes['data']['email'] = mb_strtolower($attributes['data']['email']);
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$data['root_admin'] = false;
|
||||
|
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class CanaryWidget extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.admin.widgets.canary-widget';
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static ?int $sort = 1;
|
||||
|
||||
public static function canView(): bool
|
||||
{
|
||||
return config('app.version') === 'canary';
|
||||
}
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
return [
|
||||
'actions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
|
||||
->icon('tabler-brand-github')
|
||||
->url('https://github.com/pelican-dev/panel/issues', true),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class HelpWidget extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.admin.widgets.help-widget';
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static ?int $sort = 4;
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
return [
|
||||
'actions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-help.button_docs'))
|
||||
->icon('tabler-speedboat')
|
||||
->url('https://pelican.dev/docs', true),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
|
||||
use App\Models\Node;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class NoNodesWidget extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.admin.widgets.no-nodes-widget';
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
public static function canView(): bool
|
||||
{
|
||||
return Node::count() <= 0;
|
||||
}
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
return [
|
||||
'actions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-first-node.button_label'))
|
||||
->icon('tabler-server-2')
|
||||
->url(CreateNode::getUrl()),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class SupportWidget extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.admin.widgets.support-widget';
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static ?int $sort = 3;
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
return [
|
||||
'actions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->color('success'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Widgets\Widget;
|
||||
|
||||
class UpdateWidget extends Widget
|
||||
{
|
||||
protected static string $view = 'filament.admin.widgets.update-widget';
|
||||
|
||||
protected static bool $isLazy = false;
|
||||
|
||||
protected static ?int $sort = 0;
|
||||
|
||||
private SoftwareVersionService $softwareVersionService;
|
||||
|
||||
public function mount(SoftwareVersionService $softwareVersionService): void
|
||||
{
|
||||
$this->softwareVersionService = $softwareVersionService;
|
||||
}
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
return [
|
||||
'version' => $this->softwareVersionService->currentPanelVersion(),
|
||||
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
|
||||
'isLatest' => $this->softwareVersionService->isLatestPanel(),
|
||||
'actions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('admin/dashboard.sections.intro-update-available.heading'))
|
||||
->icon('tabler-clipboard-text')
|
||||
->url('https://pelican.dev/docs/panel/update', true)
|
||||
->color('warning'),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -2,132 +2,52 @@
|
||||
|
||||
namespace App\Filament\App\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Enums\ServerResourceType;
|
||||
use App\Filament\App\Resources\ServerResource;
|
||||
use App\Filament\Components\Tables\Columns\ServerEntryColumn;
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Columns\Column;
|
||||
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
|
||||
{
|
||||
protected static string $resource = ServerResource::class;
|
||||
|
||||
public const DANGER_THRESHOLD = 0.9;
|
||||
|
||||
public const WARNING_THRESHOLD = 0.7;
|
||||
|
||||
private DaemonPowerRepository $daemonPowerRepository;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->daemonPowerRepository = new DaemonPowerRepository();
|
||||
}
|
||||
|
||||
/** @return Stack[] */
|
||||
protected function gridColumns(): array
|
||||
{
|
||||
return [
|
||||
Stack::make([
|
||||
ServerEntryColumn::make('server_entry')
|
||||
->searchable(['name']),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return Column[] */
|
||||
protected function tableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('condition')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
|
||||
->icon(fn (Server $server) => $server->condition->getIcon())
|
||||
->color(fn (Server $server) => $server->condition->getColor()),
|
||||
TextColumn::make('name')
|
||||
->label('Server')
|
||||
->description(fn (Server $server) => $server->description)
|
||||
->grow()
|
||||
->searchable(),
|
||||
TextColumn::make('allocation.address')
|
||||
->label('')
|
||||
->badge()
|
||||
->visibleFrom('md')
|
||||
->copyable(request()->isSecure()),
|
||||
TextColumn::make('cpuUsage')
|
||||
->label('Resources')
|
||||
->icon('tabler-cpu')
|
||||
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0))
|
||||
->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
|
||||
TextColumn::make('memoryUsage')
|
||||
->label('')
|
||||
->icon('tabler-device-desktop-analytics')
|
||||
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true))
|
||||
->state(fn (Server $server) => $server->formatResource('memory_bytes'))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
|
||||
TextColumn::make('diskUsage')
|
||||
->label('')
|
||||
->icon('tabler-device-sd-card')
|
||||
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true))
|
||||
->state(fn (Server $server) => $server->formatResource('disk_bytes'))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
$baseQuery = auth()->user()->accessibleServers();
|
||||
|
||||
$usingGrid = (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid';
|
||||
|
||||
return $table
|
||||
->paginated(false)
|
||||
->query(fn () => $baseQuery)
|
||||
->poll('15s')
|
||||
->columns($usingGrid ? $this->gridColumns() : $this->tableColumns())
|
||||
->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null)
|
||||
->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions()) : [])
|
||||
->actionsAlignment(Alignment::Center->value)
|
||||
->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null)
|
||||
->columns([
|
||||
Stack::make([
|
||||
ServerEntryColumn::make('server_entry')
|
||||
->searchable(['name']),
|
||||
]),
|
||||
])
|
||||
->contentGrid([
|
||||
'default' => 1,
|
||||
'md' => 2,
|
||||
])
|
||||
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!')
|
||||
->emptyStateHeading('You don\'t have access to any servers!')
|
||||
->persistFiltersInSession()
|
||||
->filters([
|
||||
SelectFilter::make('egg')
|
||||
->relationship('egg', 'name', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('egg_id')))
|
||||
->searchable()
|
||||
->preload(),
|
||||
SelectFilter::make('owner')
|
||||
->relationship('user', 'username', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('owner_id')))
|
||||
->searchable()
|
||||
->hidden(fn () => $this->activeTab === 'my')
|
||||
->preload(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatedActiveTab(): void
|
||||
{
|
||||
$this->resetTable();
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
$all = auth()->user()->accessibleServers();
|
||||
@ -147,101 +67,4 @@ class ListServers extends ListRecords
|
||||
->badge($all->count()),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getResourceColor(Server $server, string $resource): ?string
|
||||
{
|
||||
$current = null;
|
||||
$limit = null;
|
||||
|
||||
switch ($resource) {
|
||||
case 'cpu':
|
||||
$current = $server->retrieveResources()['cpu_absolute'] ?? 0;
|
||||
$limit = $server->cpu;
|
||||
if ($server->cpu === 0) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case 'memory':
|
||||
$current = $server->retrieveResources()['memory_bytes'] ?? 0;
|
||||
$limit = $server->memory * 2 ** 20;
|
||||
if ($server->memory === 0) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case 'disk':
|
||||
$current = $server->retrieveResources()['disk_bytes'] ?? 0;
|
||||
$limit = $server->disk * 2 ** 20;
|
||||
if ($server->disk === 0) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($current >= $limit * self::DANGER_THRESHOLD) {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
if ($current >= $limit * self::WARNING_THRESHOLD) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
cache()->forget("servers.$server->uuid.status");
|
||||
|
||||
$this->redirect(self::getUrl(['activeTab' => $this->activeTab]));
|
||||
} catch (ConnectionException) {
|
||||
Notification::make()
|
||||
->title(trans('exceptions.node.error_connecting', ['node' => $server->node->name]))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
/** @return Action[] */
|
||||
public static function getPowerActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('start')
|
||||
->color('primary')
|
||||
->icon('tabler-player-play-filled')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStartable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']),
|
||||
Action::make('restart')
|
||||
->color('gray')
|
||||
->icon('tabler-reload')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isRestartable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']),
|
||||
Action::make('stop')
|
||||
->color('danger')
|
||||
->icon('tabler-player-stop-filled')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStoppable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']),
|
||||
Action::make('kill')
|
||||
->color('danger')
|
||||
->icon('tabler-alert-square')
|
||||
->tooltip('This can result in data corruption and/or data loss!')
|
||||
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isKillable())
|
||||
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Arr;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class ImportEggAction extends Action
|
||||
@ -32,7 +31,7 @@ class ImportEggAction extends Action
|
||||
$this->authorize(fn () => auth()->user()->can('import egg'));
|
||||
|
||||
$this->action(function (array $data, EggImporterService $eggImportService): void {
|
||||
$eggs = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
|
||||
$eggs = array_merge($data['files'], collect($data['urls'])->flatten()->whereNotNull()->unique()->all());
|
||||
if (empty($eggs)) {
|
||||
return;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class RotateDatabasePasswordAction extends Action
|
||||
|
||||
$this->icon('tabler-refresh');
|
||||
|
||||
$this->authorize(fn (Database $database) => auth()->user()->can('update', $database));
|
||||
$this->authorize(fn (Database $database) => auth()->user()->can('update database', $database));
|
||||
|
||||
$this->modalHeading(trans('admin/databasehost.rotate_password'));
|
||||
|
||||
|
@ -17,12 +17,6 @@ class CopyFrom extends Select
|
||||
|
||||
$this->placeholder(trans('admin/egg.none'));
|
||||
|
||||
$this->preload();
|
||||
|
||||
$this->searchable();
|
||||
|
||||
$this->native(false);
|
||||
|
||||
$this->live();
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@ use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Illuminate\Support\Arr;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class ImportEggAction extends Action
|
||||
@ -32,7 +31,7 @@ class ImportEggAction extends Action
|
||||
$this->authorize(fn () => auth()->user()->can('import egg'));
|
||||
|
||||
$this->action(function (array $data, EggImporterService $eggImportService): void {
|
||||
$eggs = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
|
||||
$eggs = array_merge($data['files'], collect($data['urls'])->flatten()->whereNotNull()->unique()->all());
|
||||
if (empty($eggs)) {
|
||||
return;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ class UpdateEggAction extends Action
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label(trans_choice('admin/egg.update', 1));
|
||||
$this->label(trans('admin/egg.update'));
|
||||
|
||||
$this->icon('tabler-cloud-download');
|
||||
|
||||
@ -28,9 +28,9 @@ class UpdateEggAction extends Action
|
||||
|
||||
$this->requiresConfirmation();
|
||||
|
||||
$this->modalHeading(trans_choice('admin/egg.update_question', 1));
|
||||
$this->modalHeading(trans('admin/egg.update_question'));
|
||||
|
||||
$this->modalDescription(trans_choice('admin/egg.update_description', 1));
|
||||
$this->modalDescription(trans('admin/egg.update_description'));
|
||||
|
||||
$this->modalIconColor('danger');
|
||||
|
||||
@ -54,7 +54,7 @@ class UpdateEggAction extends Action
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans_choice('admin/egg.updated', 1))
|
||||
->title(trans('admin/egg.updated'))
|
||||
->body($egg->name)
|
||||
->success()
|
||||
->send();
|
||||
|
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Tables\Actions;
|
||||
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions\StaticAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class UpdateEggBulkAction extends BulkAction
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'update';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label(trans_choice('admin/egg.update', 2));
|
||||
|
||||
$this->icon('tabler-cloud-download');
|
||||
|
||||
$this->color('success');
|
||||
|
||||
$this->requiresConfirmation();
|
||||
|
||||
$this->modalHeading(trans_choice('admin/egg.update_question', 2));
|
||||
|
||||
$this->modalDescription(trans_choice('admin/egg.update_description', 2));
|
||||
|
||||
$this->modalIconColor('danger');
|
||||
|
||||
$this->modalSubmitAction(fn (StaticAction $action) => $action->color('danger'));
|
||||
|
||||
$this->action(function (Collection $records, EggImporterService $eggImporterService) {
|
||||
if ($records->count() === 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.no_updates'))
|
||||
->warning()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
$failed = 0;
|
||||
|
||||
/** @var Egg $egg */
|
||||
foreach ($records as $egg) {
|
||||
try {
|
||||
$eggImporterService->fromUrl($egg->update_url, $egg);
|
||||
|
||||
$success++;
|
||||
|
||||
cache()->forget("eggs.$egg->uuid.update");
|
||||
} catch (Exception $exception) {
|
||||
$failed++;
|
||||
|
||||
report($exception);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans_choice('admin/egg.updated', 2, ['count' => $success, 'total' => $records->count()]))
|
||||
->body($failed > 0 ? trans('admin/egg.updated_failed', ['count' => $failed]) : null)
|
||||
->status($failed > 0 ? 'warning' : 'success')
|
||||
->persistent()
|
||||
->send();
|
||||
});
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can('import egg'));
|
||||
|
||||
$this->deselectRecordsAfterCompletion();
|
||||
}
|
||||
}
|
@ -6,5 +6,5 @@ use Filament\Tables\Columns\Column;
|
||||
|
||||
class ServerEntryColumn extends Column
|
||||
{
|
||||
protected string $view = 'livewire.columns.server-entry-column';
|
||||
protected string $view = 'tables.columns.server-entry-column';
|
||||
}
|
||||
|
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Tables\Filters;
|
||||
|
||||
use Filament\Forms\Components\Field;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Tables\Filters\BaseFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class TagsFilter extends BaseFilter
|
||||
{
|
||||
protected string $model;
|
||||
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'tags';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->query(fn (Builder $query, array $data) => $query->when($data['tag'], fn (Builder $query, $tag) => $query->whereJsonContains('tags', $tag)));
|
||||
|
||||
$this->indicateUsing(fn (array $data) => $data['tag'] ? 'Tag: ' . $data['tag'] : null);
|
||||
|
||||
$this->resetState(['tag' => null]);
|
||||
|
||||
$this->visible(fn () => $this->getTags()->count() > 0);
|
||||
}
|
||||
|
||||
private function getTags(): Collection
|
||||
{
|
||||
return $this->getModel()::query()->pluck('tags')->flatten()->unique();
|
||||
}
|
||||
|
||||
public function getFormField(): Field
|
||||
{
|
||||
return Select::make('tag')
|
||||
->preload()
|
||||
->searchable()
|
||||
->options(fn () => $this->getTags()->mapWithKeys(fn ($tag) => [$tag => $tag]));
|
||||
}
|
||||
|
||||
public function model(string $model): static
|
||||
{
|
||||
$this->model = $model;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModel(): string
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@ use chillerlan\QRCode\QROptions;
|
||||
use DateTimeZone;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
@ -30,9 +29,7 @@ use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Auth\EditProfile as BaseEditProfile;
|
||||
use Filament\Support\Colors\Color;
|
||||
@ -41,7 +38,6 @@ 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;
|
||||
@ -129,21 +125,6 @@ class EditProfile extends BaseEditProfile
|
||||
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state])))
|
||||
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
|
||||
->native(false),
|
||||
FileUpload::make('avatar')
|
||||
->visible(fn () => config('panel.filament.uploadable-avatars'))
|
||||
->avatar()
|
||||
->acceptedFileTypes(['image/png'])
|
||||
->directory('avatars')
|
||||
->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png')
|
||||
->hintAction(function (FileUpload $fileUpload) {
|
||||
$path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png';
|
||||
|
||||
return Action::make('remove_avatar')
|
||||
->icon('tabler-photo-minus')
|
||||
->iconButton()
|
||||
->hidden(fn () => !$fileUpload->getDisk()->exists($path))
|
||||
->action(fn () => $fileUpload->getDisk()->delete($path));
|
||||
}),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.oauth'))
|
||||
@ -261,7 +242,6 @@ class EditProfile extends BaseEditProfile
|
||||
->password(),
|
||||
];
|
||||
}),
|
||||
|
||||
Tab::make(trans('profile.tabs.api_keys'))
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
@ -281,7 +261,7 @@ class EditProfile extends BaseEditProfile
|
||||
Action::make('Create')
|
||||
->label(trans('filament-actions::create.single.modal.actions.create.label'))
|
||||
->disabled(fn (Get $get) => $get('description') === null)
|
||||
->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'], panel: 'app'))
|
||||
->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab']))
|
||||
->action(function (Get $get, Action $action, User $user) {
|
||||
$token = $user->createToken(
|
||||
$get('description'),
|
||||
@ -289,8 +269,6 @@ class EditProfile extends BaseEditProfile
|
||||
);
|
||||
|
||||
Activity::event('user:api-key.create')
|
||||
->actor($user)
|
||||
->subject($user)
|
||||
->subject($token->accessToken)
|
||||
->property('identifier', $token->accessToken->identifier)
|
||||
->log();
|
||||
@ -330,11 +308,9 @@ class EditProfile extends BaseEditProfile
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.ssh_keys'))
|
||||
->icon('tabler-lock-code')
|
||||
->hidden(),
|
||||
|
||||
Tab::make(trans('profile.tabs.activity'))
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
@ -349,113 +325,6 @@ class EditProfile extends BaseEditProfile
|
||||
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.customization'))
|
||||
->icon('tabler-adjustments')
|
||||
->schema([
|
||||
Section::make(trans('profile.dashboard'))
|
||||
->collapsible()
|
||||
->icon('tabler-dashboard')
|
||||
->schema([
|
||||
ToggleButtons::make('dashboard_layout')
|
||||
->label(trans('profile.dashboard_layout'))
|
||||
->inline()
|
||||
->required()
|
||||
->options([
|
||||
'grid' => trans('profile.grid'),
|
||||
'table' => trans('profile.table'),
|
||||
]),
|
||||
]),
|
||||
Section::make(trans('profile.console'))
|
||||
->collapsible()
|
||||
->icon('tabler-brand-tabler')
|
||||
->columns(4)
|
||||
->schema([
|
||||
TextInput::make('console_font_size')
|
||||
->label(trans('profile.font_size'))
|
||||
->columnSpan(1)
|
||||
->minValue(1)
|
||||
->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),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->operation('edit')
|
||||
@ -476,7 +345,7 @@ class EditProfile extends BaseEditProfile
|
||||
$tokens = $this->toggleTwoFactorService->handle($record, $token, true);
|
||||
cache()->put("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
|
||||
|
||||
$this->redirect(self::getUrl(['tab' => '-2fa-tab'], panel: 'app'));
|
||||
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
|
||||
}
|
||||
|
||||
if ($token = $data['2fa-disable-code'] ?? null) {
|
||||
@ -512,33 +381,4 @@ 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['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout']);
|
||||
$data['customization'] = json_encode($moarbetterdata);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
$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,10 +2,8 @@
|
||||
|
||||
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;
|
||||
@ -56,38 +54,15 @@ class Login extends BaseLogin
|
||||
if ($token === null) {
|
||||
$this->verifyTwoFactor = true;
|
||||
|
||||
Activity::event('auth:checkpoint')
|
||||
->withRequestMetadata()
|
||||
->subject($user)
|
||||
->log();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$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
|
||||
Sleep::sleep(1);
|
||||
@ -133,9 +108,7 @@ class Login extends BaseLogin
|
||||
{
|
||||
return TextInput::make('2fa')
|
||||
->label(trans('auth.two-factor-code'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(trans('auth.two-factor-hint'))
|
||||
->visible(fn () => $this->verifyTwoFactor)
|
||||
->hidden(fn () => !$this->verifyTwoFactor)
|
||||
->required()
|
||||
->live();
|
||||
}
|
||||
|
@ -2,27 +2,43 @@
|
||||
|
||||
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
|
||||
{
|
||||
use EvaluatesClosures;
|
||||
protected string|Htmlable $label;
|
||||
|
||||
protected bool|Closure $copyOnClick = false;
|
||||
protected $value;
|
||||
|
||||
public function copyOnClick(bool|Closure $copyOnClick = true): static
|
||||
public function label(string|Htmlable $label): static
|
||||
{
|
||||
$this->copyOnClick = $copyOnClick;
|
||||
$this->label = $label;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function shouldCopyOnClick(): bool
|
||||
public function value($value): static
|
||||
{
|
||||
return $this->evaluate($this->copyOnClick);
|
||||
$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
|
||||
|
48
app/Filament/Server/Components/StatBlock.php
Normal file
48
app/Filament/Server/Components/StatBlock.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
@ -2,21 +2,18 @@
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Enums\ConsoleWidgetPosition;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
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;
|
||||
use App\Models\Server;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Enums\ActionSize;
|
||||
use Filament\Widgets\Widget;
|
||||
@ -25,8 +22,6 @@ use Livewire\Attributes\On;
|
||||
|
||||
class Console extends Page
|
||||
{
|
||||
use InteractsWithActions;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-brand-tabler';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
@ -43,38 +38,14 @@ class Console extends Page
|
||||
try {
|
||||
$server->validateCurrentState();
|
||||
} catch (ServerStateConflictException $exception) {
|
||||
AlertBanner::make('server_conflict')
|
||||
AlertBanner::make()
|
||||
->warning()
|
||||
->title('Warning')
|
||||
->body($exception->getMessage())
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
/** @var FeatureProvider $feature */
|
||||
foreach ($server->egg->features() as $feature) {
|
||||
$this->cacheAction($feature->getAction());
|
||||
}
|
||||
}
|
||||
|
||||
#[On('mount-feature')]
|
||||
public function mountFeature(string $data): void
|
||||
{
|
||||
$data = json_decode($data);
|
||||
$feature = data_get($data, 'key');
|
||||
|
||||
$feature = FeatureProvider::getProviders($feature);
|
||||
if ($this->getMountedAction()) {
|
||||
return;
|
||||
}
|
||||
$this->mountAction($feature->getId());
|
||||
sleep(2); // TODO find a better way
|
||||
}
|
||||
|
||||
public function getWidgetData(): array
|
||||
{
|
||||
return [
|
||||
@ -83,41 +54,18 @@ class Console extends Page
|
||||
];
|
||||
}
|
||||
|
||||
/** @var array<string, array<class-string<Widget>>> */
|
||||
protected static array $customWidgets = [];
|
||||
|
||||
/** @param class-string<Widget>[] $customWidgets */
|
||||
public static function registerCustomWidgets(ConsoleWidgetPosition $position, array $customWidgets): void
|
||||
{
|
||||
static::$customWidgets[$position->value] = array_unique(array_merge(static::$customWidgets[$position->value] ?? [], $customWidgets));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<Widget>[]
|
||||
*/
|
||||
public function getWidgets(): array
|
||||
{
|
||||
$allWidgets = [];
|
||||
|
||||
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Top->value] ?? []);
|
||||
|
||||
$allWidgets[] = ServerOverview::class;
|
||||
|
||||
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::AboveConsole->value] ?? []);
|
||||
|
||||
$allWidgets[] = ServerConsole::class;
|
||||
|
||||
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::BelowConsole->value] ?? []);
|
||||
|
||||
$allWidgets = array_merge($allWidgets, [
|
||||
return [
|
||||
ServerOverview::class,
|
||||
ServerConsole::class,
|
||||
ServerCpuChart::class,
|
||||
ServerMemoryChart::class,
|
||||
ServerNetworkChart::class,
|
||||
]);
|
||||
|
||||
$allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Bottom->value] ?? []);
|
||||
|
||||
return array_unique($allWidgets);
|
||||
//ServerNetworkChart::class, TODO: convert units.
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,33 +102,32 @@ class Console extends Page
|
||||
Action::make('start')
|
||||
->color('primary')
|
||||
->size(ActionSize::ExtraLarge)
|
||||
->dispatch('setServerState', ['state' => 'start', 'uuid' => $server->uuid])
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'start', uuid: $server->uuid))
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
|
||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable())
|
||||
->icon('tabler-player-play-filled'),
|
||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable()),
|
||||
Action::make('restart')
|
||||
->color('gray')
|
||||
->size(ActionSize::ExtraLarge)
|
||||
->dispatch('setServerState', ['state' => 'restart', 'uuid' => $server->uuid])
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'restart', uuid: $server->uuid))
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
|
||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable())
|
||||
->icon('tabler-reload'),
|
||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable()),
|
||||
Action::make('stop')
|
||||
->color('danger')
|
||||
->size(ActionSize::ExtraLarge)
|
||||
->dispatch('setServerState', ['state' => 'stop', 'uuid' => $server->uuid])
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'stop', uuid: $server->uuid))
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->hidden(fn () => $this->status->isStartingOrStopping() || $this->status->isKillable())
|
||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable())
|
||||
->icon('tabler-player-stop-filled'),
|
||||
->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable()),
|
||||
Action::make('kill')
|
||||
->color('danger')
|
||||
->tooltip('This can result in data corruption and/or data loss!')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Do you wish to kill this server?')
|
||||
->modalDescription('This can result in data corruption and/or data loss!')
|
||||
->modalSubmitActionLabel('Kill Server')
|
||||
->size(ActionSize::ExtraLarge)
|
||||
->dispatch('setServerState', ['state' => 'kill', 'uuid' => $server->uuid])
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'kill', uuid: $server->uuid))
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable())
|
||||
->icon('tabler-alert-square'),
|
||||
->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ class Settings extends ServerFormPage
|
||||
]),
|
||||
Section::make('Reinstall Server')
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
|
||||
->collapsible()
|
||||
->collapsible()->collapsed()
|
||||
->footerActions([
|
||||
Action::make('reinstall')
|
||||
->color('danger')
|
||||
@ -226,8 +226,6 @@ class Settings extends ServerFormPage
|
||||
->success()
|
||||
->title('Server Reinstall started')
|
||||
->send();
|
||||
|
||||
redirect(Console::getUrl());
|
||||
}),
|
||||
])
|
||||
->footerActionsAlignment(Alignment::Right)
|
||||
@ -259,6 +257,7 @@ class Settings extends ServerFormPage
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Updated Server Name')
|
||||
->body(fn () => $original . ' -> ' . $name)
|
||||
->send();
|
||||
@ -290,6 +289,7 @@ class Settings extends ServerFormPage
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Updated Server Description')
|
||||
->body(fn () => $original . ' -> ' . $description)
|
||||
->send();
|
||||
|
@ -18,7 +18,6 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class Startup extends ServerFormPage
|
||||
@ -101,7 +100,7 @@ class Startup extends ServerFormPage
|
||||
->schema([
|
||||
Repeater::make('server_variables')
|
||||
->label('')
|
||||
->relationship('serverVariables', fn (Builder $query) => $query->where('egg_variables.user_viewable', true)->orderByPowerJoins('variable.sort'))
|
||||
->relationship('viewableServerVariables')
|
||||
->grid()
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
|
||||
->reorderable(false)->addable(false)->deletable(false)
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
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;
|
||||
@ -11,20 +9,9 @@ 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
|
||||
{
|
||||
@ -38,101 +25,13 @@ 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)->where('subject_type', $server->getMorphClass()))
|
||||
return $server->activity()
|
||||
->getQuery()
|
||||
->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
|
||||
@ -153,6 +52,11 @@ 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,13 +2,113 @@
|
||||
|
||||
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\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, 100, 250])
|
||||
->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) => collect($state)->filter(fn ($item) => !is_array($item))->all()),
|
||||
]),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('event')
|
||||
->options(fn (Table $table) => $table->getQuery()->pluck('event', 'event')->unique()->sort())
|
||||
->searchable()
|
||||
->preload(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
|
@ -2,18 +2,12 @@
|
||||
|
||||
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
|
||||
@ -28,61 +22,6 @@ 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,24 +4,85 @@ 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\Action;
|
||||
use Filament\Actions;
|
||||
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 [
|
||||
Action::make('addAllocation')
|
||||
Actions\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'))
|
||||
@ -32,7 +93,7 @@ class ListAllocations extends ListRecords
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
}),
|
||||
];
|
||||
|
@ -2,37 +2,13 @@
|
||||
|
||||
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
|
||||
{
|
||||
@ -68,138 +44,8 @@ class BackupResource extends Resource
|
||||
return null;
|
||||
}
|
||||
|
||||
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),
|
||||
]),
|
||||
]);
|
||||
return $count >= $limit ? 'danger'
|
||||
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
|
@ -2,28 +2,158 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\BackupResource\Pages;
|
||||
|
||||
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 Filament\Actions\CreateAction;
|
||||
use App\Filament\Components\Tables\Columns\BytesColumn;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
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 Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
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;
|
||||
|
||||
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(),
|
||||
IconColumn::make('is_successful')
|
||||
->label('Successful')
|
||||
->boolean(),
|
||||
IconColumn::make('is_locked')
|
||||
->visibleFrom('md')
|
||||
->label('Lock Status')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock-open' : 'tabler-lock'),
|
||||
])
|
||||
->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)),
|
||||
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),
|
||||
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();
|
||||
}),
|
||||
DeleteAction::make('delete')
|
||||
->disabled(fn (Backup $backup): bool => $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)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
CreateAction::make()
|
||||
Actions\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)
|
||||
@ -36,7 +166,6 @@ class ListBackups extends ListRecords
|
||||
$action->setIsLocked((bool) $data['is_locked']);
|
||||
}
|
||||
|
||||
try {
|
||||
$backup = $action->handle($server, $data['name']);
|
||||
|
||||
Activity::event('server:backup.start')
|
||||
@ -49,13 +178,6 @@ class ListBackups extends ListRecords
|
||||
->body($backup->name . ' created.')
|
||||
->success()
|
||||
->send();
|
||||
} catch (HttpException $e) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Failed')
|
||||
->body($e->getMessage() . ' Try again' . ($e->getHeaders()['Retry-After'] ? ' in ' . $e->getHeaders()['Retry-After'] . ' seconds.' : ''))
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
@ -2,23 +2,13 @@
|
||||
|
||||
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
|
||||
{
|
||||
@ -52,65 +42,9 @@ class DatabaseResource extends Resource
|
||||
return null;
|
||||
}
|
||||
|
||||
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)),
|
||||
]);
|
||||
return $count >= $limit
|
||||
? 'danger'
|
||||
: ($count >= $limit * self::WARNING_THRESHOLD ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
|
@ -2,8 +2,13 @@
|
||||
|
||||
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;
|
||||
@ -11,12 +16,81 @@ 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 */
|
||||
|
@ -4,8 +4,6 @@ namespace App\Filament\Server\Resources\FileResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Enums\EditorLanguages;
|
||||
use App\Exceptions\Http\Server\FileSizeTooLargeException;
|
||||
use App\Exceptions\Repository\FileNotEditableException;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Livewire\AlertBanner;
|
||||
@ -26,8 +24,6 @@ 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;
|
||||
@ -49,8 +45,6 @@ class EditFiles extends Page
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
private DaemonFileRepository $fileRepository;
|
||||
|
||||
/** @var array<mixed> */
|
||||
public ?array $data = [];
|
||||
|
||||
@ -72,8 +66,12 @@ class EditFiles extends Page
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->icon('tabler-device-floppy')
|
||||
->keyBindings('mod+shift+s')
|
||||
->action(function () {
|
||||
$this->getDaemonFileRepository()->putContent($this->path, $this->data['editor'] ?? '');
|
||||
->action(function (DaemonFileRepository $fileRepository) use ($server) {
|
||||
$data = $this->form->getState();
|
||||
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->putContent($this->path, $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', $this->path)
|
||||
@ -92,8 +90,12 @@ class EditFiles extends Page
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->icon('tabler-device-floppy')
|
||||
->keyBindings('mod+s')
|
||||
->action(function () {
|
||||
$this->getDaemonFileRepository()->putContent($this->path, $this->data['editor'] ?? '');
|
||||
->action(function (DaemonFileRepository $fileRepository) use ($server) {
|
||||
$data = $this->form->getState();
|
||||
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->putContent($this->path, $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', $this->path)
|
||||
@ -115,48 +117,21 @@ class EditFiles extends Page
|
||||
->schema([
|
||||
Select::make('lang')
|
||||
->label('Syntax Highlighting')
|
||||
->searchable()
|
||||
->native(false)
|
||||
->live()
|
||||
->options(EditorLanguages::class)
|
||||
->selectablePlaceholder(false)
|
||||
->afterStateUpdated(fn ($state) => $this->dispatch('setLanguage', lang: $state))
|
||||
->default(fn () => EditorLanguages::fromWithAlias(pathinfo($this->path, PATHINFO_EXTENSION))),
|
||||
MonacoEditor::make('editor')
|
||||
->hiddenLabel()
|
||||
->showPlaceholder(false)
|
||||
->default(function () {
|
||||
->label('')
|
||||
->placeholderText('')
|
||||
->default(function (DaemonFileRepository $fileRepository) use ($server) {
|
||||
try {
|
||||
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
|
||||
} catch (FileSizeTooLargeException) {
|
||||
AlertBanner::make()
|
||||
->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(['path' => dirname($this->path)]));
|
||||
return $fileRepository
|
||||
->setServer($server)
|
||||
->getContent($this->path, config('panel.files.max_edit_size'));
|
||||
} catch (FileNotFoundException) {
|
||||
AlertBanner::make()
|
||||
->title('<code>' . basename($this->path) . '</code> not found!')
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
} catch (FileNotEditableException) {
|
||||
AlertBanner::make()
|
||||
->title('<code>' . basename($this->path) . '</code> is a directory')
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
$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)]));
|
||||
abort(404, $this->path . ' not found.');
|
||||
}
|
||||
})
|
||||
->language(fn (Get $get) => $get('lang'))
|
||||
@ -174,21 +149,12 @@ class EditFiles extends Page
|
||||
$this->form->fill();
|
||||
|
||||
if (str($path)->endsWith('.pelicanignore')) {
|
||||
AlertBanner::make('.pelicanignore_info')
|
||||
AlertBanner::make()
|
||||
->title('You\'re editing a <code>.pelicanignore</code> file!')
|
||||
->body('Any files or directories listed in here will be excluded from backups. Wildcards are supported by using an asterisk (<code>*</code>).<br>You can negate a prior rule by prepending an exclamation point (<code>!</code>).')
|
||||
->info()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
try {
|
||||
$this->getDaemonFileRepository()->getDirectory('/');
|
||||
} catch (ConnectionException) {
|
||||
AlertBanner::make('node_connection_error')
|
||||
->title('Could not connect to the node!')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,23 +200,6 @@ class EditFiles extends Page
|
||||
return $breadcrumbs;
|
||||
}
|
||||
|
||||
private function getDaemonFileRepository(): DaemonFileRepository
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
$this->fileRepository ??= (new DaemonFileRepository())->setServer($server);
|
||||
|
||||
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(
|
||||
|
@ -29,6 +29,7 @@ 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;
|
||||
@ -37,7 +38,6 @@ use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
@ -46,9 +46,13 @@ class ListFiles extends ListRecords
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
#[Locked]
|
||||
public string $path = '/';
|
||||
public string $path;
|
||||
|
||||
private DaemonFileRepository $fileRepository;
|
||||
public function mount(?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
$this->path = $path ?? '/';
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
@ -72,12 +76,10 @@ class ListFiles extends ListRecords
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$files = File::get($server, $this->path);
|
||||
|
||||
return $table
|
||||
->paginated([25, 50])
|
||||
->defaultPaginationPageOption(25)
|
||||
->query(fn () => $files->orderByDesc('is_directory'))
|
||||
->paginated([25, 50, 100, 250])
|
||||
->defaultPaginationPageOption(50)
|
||||
->query(fn () => File::get($server, $this->path)->orderByDesc('is_directory'))
|
||||
->defaultSort('name')
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
@ -86,7 +88,6 @@ class ListFiles extends ListRecords
|
||||
->icon(fn (File $file) => $file->getIcon()),
|
||||
BytesColumn::make('size')
|
||||
->visibleFrom('md')
|
||||
->state(fn (File $file) => $file->is_directory ? null : $file->size)
|
||||
->sortable(),
|
||||
DateTimeColumn::make('modified_at')
|
||||
->visibleFrom('md')
|
||||
@ -127,10 +128,12 @@ class ListFiles extends ListRecords
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required(),
|
||||
])
|
||||
->action(function ($data, File $file) {
|
||||
->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$files = [['to' => $data['name'], 'from' => $file->name]];
|
||||
|
||||
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
@ -150,8 +153,10 @@ class ListFiles extends ListRecords
|
||||
->label('Copy')
|
||||
->icon('tabler-copy')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->action(function (File $file) {
|
||||
$this->getDaemonFileRepository()->copyFile(join_paths($this->path, $file->name));
|
||||
->action(function (File $file, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->copyFile(join_paths($this->path, $file->name));
|
||||
|
||||
Activity::event('server:file.copy')
|
||||
->property('file', join_paths($this->path, $file->name))
|
||||
@ -176,32 +181,32 @@ class ListFiles extends ListRecords
|
||||
->icon('tabler-replace')
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('New location')
|
||||
->hint('Enter the location of this file or folder, relative to the current directory.')
|
||||
->label('File name')
|
||||
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get, File $file) => resolve_path('./' . join_paths($this->path, $get('location') ?? '/', $file->name))),
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location')))),
|
||||
])
|
||||
->action(function ($data, File $file) {
|
||||
$location = rtrim($data['location'], '/');
|
||||
$files = [['to' => join_paths($location, $file->name), 'from' => $file->name]];
|
||||
->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$location = resolve_path(join_paths($this->path, $data['location']));
|
||||
|
||||
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
|
||||
$files = [['to' => $location, 'from' => $file->name]];
|
||||
|
||||
$oldLocation = join_paths($this->path, $file->name);
|
||||
$newLocation = resolve_path(join_paths($this->path, $location, $file->name));
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->property('to', $newLocation)
|
||||
->property('from', $oldLocation)
|
||||
->property('to', $location)
|
||||
->property('from', $file->name)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File Moved')
|
||||
->body($oldLocation . ' -> ' . $newLocation)
|
||||
->title(join_paths($this->path, $file->name) . ' was moved to ' . $location)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
@ -247,14 +252,16 @@ class ListFiles extends ListRecords
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
])
|
||||
->action(function ($data, File $file) {
|
||||
->action(function ($data, File $file, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$owner = (in_array('read', $data['owner']) ? 4 : 0) | (in_array('write', $data['owner']) ? 2 : 0) | (in_array('execute', $data['owner']) ? 1 : 0);
|
||||
$group = (in_array('read', $data['group']) ? 4 : 0) | (in_array('write', $data['group']) ? 2 : 0) | (in_array('execute', $data['group']) ? 1 : 0);
|
||||
$public = (in_array('read', $data['public']) ? 4 : 0) | (in_array('write', $data['public']) ? 2 : 0) | (in_array('execute', $data['public']) ? 1 : 0);
|
||||
|
||||
$mode = $owner . $group . $public;
|
||||
|
||||
$this->getDaemonFileRepository()->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
|
||||
|
||||
Notification::make()
|
||||
->title('Permissions changed to ' . $mode)
|
||||
@ -265,24 +272,18 @@ class ListFiles extends ListRecords
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->label('Archive')
|
||||
->icon('tabler-archive')
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Archive name')
|
||||
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
||||
->suffix('.tar.gz'),
|
||||
])
|
||||
->action(function ($data, File $file) {
|
||||
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, [$file->name], $data['name']);
|
||||
->action(function (File $file, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->compressFiles($this->path, [$file->name]);
|
||||
|
||||
Activity::event('server:file.compress')
|
||||
->property('name', $archive['name'])
|
||||
->property('directory', $this->path)
|
||||
->property('files', [$file->name])
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->body($archive['name'])
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@ -293,8 +294,10 @@ class ListFiles extends ListRecords
|
||||
->label('Unarchive')
|
||||
->icon('tabler-archive')
|
||||
->visible(fn (File $file) => $file->isArchive())
|
||||
->action(function (File $file) {
|
||||
$this->getDaemonFileRepository()->decompressFile($this->path, $file->name);
|
||||
->action(function (File $file, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->decompressFile($this->path, $file->name);
|
||||
|
||||
Activity::event('server:file.decompress')
|
||||
->property('directory', $this->path)
|
||||
@ -314,10 +317,12 @@ class ListFiles extends ListRecords
|
||||
->label('')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->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]);
|
||||
->modalDescription(fn (File $file) => $file->name)
|
||||
->modalHeading('Delete file?')
|
||||
->action(function (File $file, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->deleteFiles($this->path, [$file->name]);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $this->path)
|
||||
@ -325,23 +330,28 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
}),
|
||||
])
|
||||
->groupedBulkActions([
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
BulkAction::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->hidden() // TODO
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('Directory')
|
||||
->hint('Enter the new directory, relative to the current directory.')
|
||||
->label('File name')
|
||||
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
|
||||
->default(fn (File $file) => $file->name)
|
||||
->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'], '/');
|
||||
->action(function (Collection $files, $data, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$location = resolve_path(join_paths($this->path, $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' => $location, 'from' => $file['name']])->toArray();
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
@ -349,32 +359,26 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
|
||||
->title(count($files) . ' Files were moved from ' . $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) {
|
||||
->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||
|
||||
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->compressFiles($this->path, $files);
|
||||
|
||||
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();
|
||||
|
||||
@ -382,9 +386,11 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->action(function (Collection $files) {
|
||||
->action(function (Collection $files, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$files = $files->map(fn ($file) => $file['name'])->toArray();
|
||||
$this->getDaemonFileRepository()->deleteFiles($this->path, $files);
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->deleteFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $this->path)
|
||||
@ -396,6 +402,7 @@ class ListFiles extends ListRecords
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -411,8 +418,10 @@ class ListFiles extends ListRecords
|
||||
->color('gray')
|
||||
->keyBindings('')
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function ($data) {
|
||||
$this->getDaemonFileRepository()->putContent(join_paths($this->path, $data['name']), $data['editor'] ?? '');
|
||||
->action(function ($data, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->putContent(join_paths($this->path, $data['name']), $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', join_paths($this->path, $data['name']))
|
||||
@ -424,8 +433,6 @@ class ListFiles extends ListRecords
|
||||
->required(),
|
||||
Select::make('lang')
|
||||
->label('Syntax Highlighting')
|
||||
->searchable()
|
||||
->native(false)
|
||||
->live()
|
||||
->options(EditorLanguages::class)
|
||||
->selectablePlaceholder(false)
|
||||
@ -440,8 +447,10 @@ class ListFiles extends ListRecords
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('New Folder')
|
||||
->color('gray')
|
||||
->action(function ($data) {
|
||||
$this->getDaemonFileRepository()->createDirectory($data['name'], $this->path);
|
||||
->action(function ($data, DaemonFileRepository $fileRepository) use ($server) {
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->createDirectory($data['name'], $this->path);
|
||||
|
||||
Activity::event('server:file.create-directory')
|
||||
->property(['directory' => $this->path, 'name' => $data['name']])
|
||||
@ -455,11 +464,13 @@ class ListFiles extends ListRecords
|
||||
HeaderAction::make('upload')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('Upload')
|
||||
->action(function ($data) {
|
||||
->action(function ($data, DaemonFileRepository $fileRepository) use ($server) {
|
||||
if (count($data['files']) > 0 && !isset($data['url'])) {
|
||||
/** @var UploadedFile $file */
|
||||
foreach ($data['files'] as $file) {
|
||||
$this->getDaemonFileRepository()->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
|
||||
|
||||
Activity::event('server:file.uploaded')
|
||||
->property('directory', $this->path)
|
||||
@ -467,7 +478,9 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
}
|
||||
} elseif ($data['url'] !== null) {
|
||||
$this->getDaemonFileRepository()->pull($data['url'], $this->path);
|
||||
$fileRepository
|
||||
->setServer($server)
|
||||
->pull($data['url'], $this->path);
|
||||
|
||||
Activity::event('server:file.pull')
|
||||
->property('url', $data['url'])
|
||||
@ -508,9 +521,8 @@ class ListFiles extends ListRecords
|
||||
->form([
|
||||
TextInput::make('searchTerm')
|
||||
->placeholder('Enter a search term, e.g. *.txt')
|
||||
->required()
|
||||
->regex('/^[^*]*\*?[^*]*$/')
|
||||
->minValue(3),
|
||||
->minLength(3),
|
||||
])
|
||||
->action(fn ($data) => redirect(SearchFiles::getUrl([
|
||||
'searchTerm' => $data['searchTerm'],
|
||||
@ -519,32 +531,6 @@ class ListFiles extends ListRecords
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getPermissionsFromModeBit(int $mode): array
|
||||
{
|
||||
return match ($mode) {
|
||||
1 => ['execute'],
|
||||
2 => ['write'],
|
||||
3 => ['write', 'execute'],
|
||||
4 => ['read'],
|
||||
5 => ['read', 'execute'],
|
||||
6 => ['read', 'write'],
|
||||
7 => ['read', 'write', 'execute'],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
private function getDaemonFileRepository(): DaemonFileRepository
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
$this->fileRepository ??= (new DaemonFileRepository())->setServer($server);
|
||||
|
||||
return $this->fileRepository;
|
||||
}
|
||||
|
||||
public static function route(string $path): PageRegistration
|
||||
{
|
||||
return new PageRegistration(
|
||||
@ -555,4 +541,28 @@ class ListFiles extends ListRecords
|
||||
->where('path', '.*'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getPermissionsFromModeBit(int $mode): array
|
||||
{
|
||||
if ($mode === 1) {
|
||||
return ['execute'];
|
||||
} elseif ($mode === 2) {
|
||||
return ['write'];
|
||||
} elseif ($mode === 3) {
|
||||
return ['write', 'execute'];
|
||||
} elseif ($mode === 4) {
|
||||
return ['read'];
|
||||
} elseif ($mode === 5) {
|
||||
return ['read', 'execute'];
|
||||
} elseif ($mode === 6) {
|
||||
return ['read', 'write'];
|
||||
} elseif ($mode === 7) {
|
||||
return ['read', 'write', 'execute'];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ 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
|
||||
{
|
||||
@ -23,8 +22,15 @@ class SearchFiles extends ListRecords
|
||||
#[Locked]
|
||||
public string $searchTerm;
|
||||
|
||||
#[Url]
|
||||
public string $path = '/';
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public function mount(?string $searchTerm = null, ?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->path = $path ?? '/';
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
|
@ -2,16 +2,11 @@
|
||||
|
||||
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;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
@ -22,15 +17,7 @@ use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
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
|
||||
@ -311,44 +298,6 @@ 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 [
|
||||
@ -365,18 +314,4 @@ class ScheduleResource extends Resource
|
||||
'edit' => Pages\EditSchedule::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
|
||||
{
|
||||
try {
|
||||
return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek);
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->title('The cron data provided does not evaluate to a valid expression')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,14 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Helpers\Utilities;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
@ -35,18 +39,27 @@ class CreateSchedule extends CreateRecord
|
||||
}
|
||||
|
||||
if (!isset($data['next_run_at'])) {
|
||||
$data['next_run_at'] = ScheduleResource::getNextRun(
|
||||
$data['cron_minute'],
|
||||
$data['cron_hour'],
|
||||
$data['cron_day_of_month'],
|
||||
$data['cron_month'],
|
||||
$data['cron_day_of_week']
|
||||
);
|
||||
$data['next_run_at'] = $this->getNextRunAt($data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], $data['cron_day_of_week']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getNextRunAt(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
|
||||
{
|
||||
try {
|
||||
return Utilities::getScheduleNextRunDate(
|
||||
$minute,
|
||||
$hour,
|
||||
$dayOfMonth,
|
||||
$month,
|
||||
$dayOfWeek
|
||||
);
|
||||
} catch (Exception) {
|
||||
throw new DisplayException('The cron data provided does not evaluate to a valid expression.');
|
||||
}
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
|
@ -22,19 +22,6 @@ class EditSchedule extends EditRecord
|
||||
->log();
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$data['next_run_at'] = ScheduleResource::getNextRun(
|
||||
$data['cron_minute'],
|
||||
$data['cron_hour'],
|
||||
$data['cron_day_of_month'],
|
||||
$data['cron_month'],
|
||||
$data['cron_day_of_week']
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
@ -2,19 +2,63 @@
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use App\Models\Schedule;
|
||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
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')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->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 [
|
||||
CreateAction::make()
|
||||
->label('New Schedule'),
|
||||
Actions\CreateAction::make()->label('New Schedule'),
|
||||
];
|
||||
}
|
||||
|
||||
|
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