Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
a5a4e01c76 ci(release): bump version 2025-11-09 17:31:45 +00:00
1154 changed files with 14216 additions and 33295 deletions

View File

@@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
php: [8.2, 8.3, 8.4]
env:
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
@@ -61,17 +61,14 @@ jobs:
- name: Create SQLite file
run: touch database/testing.sqlite
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration
mysql:
name: MySQL
@@ -79,7 +76,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
php: [8.2, 8.3, 8.4]
database: ["mysql:8"]
services:
database:
@@ -123,20 +120,14 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@@ -147,7 +138,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
php: [8.2, 8.3, 8.4]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
services:
database:
@@ -191,20 +182,14 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@@ -215,7 +200,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
php: [8.2, 8.3, 8.4]
database: ["postgres:14"]
services:
database:
@@ -253,7 +238,6 @@ jobs:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -266,14 +250,11 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration

View File

@@ -3,7 +3,7 @@ name: Lint
on:
pull_request:
branches:
- "**"
- '**'
jobs:
pint:
@@ -16,7 +16,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
php-version: "8.3"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
php: [ 8.2, 8.3, 8.4 ]
steps:
- name: Code Checkout
uses: actions/checkout@v4

View File

@@ -2,7 +2,7 @@
# Pelican Production Dockerfile
##
# If you want to build this locally you want to run `docker build -f Dockerfile.dev .`
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
##
# ================================
@@ -38,7 +38,7 @@ RUN yarn config set network-timeout 300000 \
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=docker/ . ./
COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize
@@ -50,7 +50,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=docker/ . ./
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
@@ -62,34 +62,37 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
# packages for running the panel
caddy ca-certificates supervisor supercronic fcgi \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
caddy ca-certificates supervisor supercronic fcgi
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Create and remove directories
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
&& rm -rf /var/www/html/plugins \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env /var/www/html/.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -s /pelican-data/storage /var/www/html/public/storage \
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
&& ln -s /pelican-data/plugins /var/www/html \
# Allow www-data write permissions where necessary
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
# 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 \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/crontabs/crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh

View File

@@ -5,6 +5,6 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-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 pcntl pdo_mysql pdo_pgsql bz2
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions

View File

@@ -5,7 +5,7 @@ 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 pcntl pdo_mysql pdo_pgsql bz2
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions
@@ -42,7 +42,7 @@ RUN yarn config set network-timeout 300000 \
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=docker/ . ./
COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize
@@ -54,7 +54,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=docker/ . ./
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
@@ -68,33 +68,35 @@ WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
# packages for running the panel
caddy ca-certificates supervisor supercronic fcgi coreutils \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
caddy ca-certificates supervisor supercronic fcgi coreutils
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Create and remove directories
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
&& rm -rf /var/www/html/plugins \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env /var/www/html/.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -s /pelican-data/storage /var/www/html/public/storage \
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
&& ln -s /pelican-data/plugins /var/www/html \
# Allow www-data write permissions where necessary
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
# 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 \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/crontabs/crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Console\Commands\Dev;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class GenerateTablerIconsEnum extends Command
{
protected $signature = 'dev:generate-tabler-icons-enum';
protected $description = 'Generate an enum for tabler icons based on the secondnetwork/blade-tabler-icons svgs';
public function handle(): void
{
$files = File::files(base_path('vendor/secondnetwork/blade-tabler-icons/resources/svg'));
$files = array_filter($files, fn ($file) => $file->getExtension() === 'svg');
$enumContent = "<?php\n\n";
$enumContent .= "namespace App\\Enums;\n\n";
$enumContent .= "enum TablerIcon: string\n{\n";
foreach ($files as $file) {
$filename = pathinfo($file->getFilename(), PATHINFO_FILENAME);
// Letter V is duplicate, as "letter-v" and "letter-letter-v"
if (str($filename)->contains('letter-letter')) {
continue;
}
// Filled icons exist with "-f" and "-filled", we only want the later
if (str($filename)->endsWith('-f') && file_exists(base_path("vendor/secondnetwork/blade-tabler-icons/resources/svg/{$filename}illed.svg"))) {
continue;
}
$caseName = str($filename)->title()->replace('-', '');
$value = str($filename)->slug()->prepend('tabler-');
$enumContent .= " case $caseName = '$value';\n";
}
$enumContent .= "}\n";
File::put(base_path('app/Enums/TablerIcon.php'), $enumContent);
$this->info('Enum generated');
}
}

View File

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

View File

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

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Console\Commands\Overrides;
use Illuminate\Foundation\Console\ConfigCacheCommand as BaseConfigCacheCommand;
class ConfigCacheCommand extends BaseConfigCacheCommand
{
/**
* Prevent config from being cached
*/
public function handle()
{
$this->components->warn('Configuration caching has been disabled.');
$this->line(' Reason: This application uses dynamic plugins. Caching config');
$this->line(' prevents /plugins/config/*.php files from being loaded correctly.');
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Console\Commands\Overrides;
use Illuminate\Foundation\Console\OptimizeCommand as BaseOptimizeCommand;
class OptimizeCommand extends BaseOptimizeCommand
{
/**
* Prevent config from being cached
*
* @return array<string, string>
*/
protected function getOptimizeTasks()
{
return array_except(parent::getOptimizeTasks(), 'config');
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class ComposerPluginsCommand extends Command
{
protected $signature = 'p:plugin:composer';
protected $description = 'Makes sure the needed composer packages for all installed plugins are available.';
public function handle(PluginService $pluginService): void
{
try {
$pluginService->manageComposerPackages();
} catch (Exception $exception) {
report($exception);
$this->error($exception->getMessage());
}
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Illuminate\Console\Command;
class DisablePluginCommand extends Command
{
protected $signature = 'p:plugin:disable {id?}';
protected $description = 'Disables a plugin';
public function handle(PluginService $pluginService): void
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
if (!$plugin) {
$this->error('Plugin does not exist!');
return;
}
if (!$plugin->canDisable()) {
$this->error("Plugin can't be disabled!");
return;
}
$pluginService->disablePlugin($plugin);
$this->info('Plugin disabled.');
}
}

View File

@@ -1,43 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class InstallPluginCommand extends Command
{
protected $signature = 'p:plugin:install {id?}';
protected $description = 'Installs a plugin';
public function handle(PluginService $pluginService): void
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
if (!$plugin) {
$this->error('Plugin does not exist!');
return;
}
if ($plugin->status !== PluginStatus::NotInstalled) {
$this->error('Plugin is already installed!');
return;
}
try {
$pluginService->installPlugin($plugin);
$this->info('Plugin installed and enabled.');
} catch (Exception $exception) {
$this->error('Could not install plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Models\Plugin;
use Illuminate\Console\Command;
class ListPluginsCommand extends Command
{
protected $signature = 'p:plugin:list';
protected $description = 'List all installed plugins';
public function handle(): void
{
$plugins = Plugin::query()->get(['name', 'author', 'status', 'version', 'panels', 'category']);
if (count($plugins) < 1) {
$this->warn('No plugins installed');
return;
}
$this->table(['Name', 'Author', 'Status', 'Version', 'Panels', 'Category'], $plugins->toArray());
$this->output->newLine();
}
}

View File

@@ -1,135 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Enums\PluginCategory;
use App\Enums\PluginStatus;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
class MakePluginCommand extends Command
{
protected $signature = 'p:plugin:make
{--name=}
{--author=}
{--description=}
{--category=}
{--url=}
{--updateUrl=}
{--panels=}
{--panelVersion=}';
protected $description = 'Create a new plugin';
public function __construct(private Filesystem $filesystem)
{
parent::__construct();
}
public function handle(): void
{
$name = $this->option('name') ?? $this->ask('Name');
$name = preg_replace('/[^A-Za-z0-9 ]/', '', Str::ascii($name));
$id = Str::slug($name);
if ($this->filesystem->exists(plugin_path($id))) {
$this->error('Plugin with that name already exists!');
return;
}
$author = $this->option('author') ?? $this->ask('Author', cache('plugin.author'));
$author = preg_replace('/[^A-Za-z0-9 ]/', '', Str::ascii($author));
cache()->forever('plugin.author', $author);
$namespace = Str::studly($author) . '\\' . Str::studly($name);
$class = Str::studly($name . 'Plugin');
if (class_exists('\\' . $namespace . '\\' . $class)) {
$this->error('Plugin class with that name already exists!');
return;
}
$this->info('Creating Plugin "' . $name . '" (' . $id . ') by ' . $author);
$description = $this->option('description') ?? $this->ask('Description (can be empty)');
$category = $this->option('category') ?? $this->choice('Category', collect(PluginCategory::cases())->mapWithKeys(fn (PluginCategory $category) => [$category->value => $category->getLabel()])->toArray(), PluginCategory::Plugin->value);
if (!PluginCategory::tryFrom($category)) {
$this->error('Unknown plugin category!');
return;
}
$url = $this->option('url') ?? $this->ask('URL (can be empty)');
$updateUrl = $this->option('updateUrl') ?? $this->ask('Update URL (can be empty)');
$panels = $this->option('panels');
if (!$panels) {
if ($this->confirm('Should the plugin be available on all panels?', true)) {
$panels = null;
} else {
$panels = $this->choice('Panels (comma separated list)', [
'admin' => 'Admin Area',
'server' => 'Client Area',
'app' => 'Server List',
], multiple: true);
}
}
$panels = is_string($panels) ? explode(',', $panels) : $panels;
$panelVersion = $this->option('panelVersion');
if (!$panelVersion) {
$panelVersion = $this->ask('Required panel version (leave empty for no constraint)', config('app.version') === 'canary' ? null : config('app.version'));
if ($panelVersion && $this->confirm("Should the version constraint be minimal instead of strict? ($panelVersion or higher instead of only $panelVersion)")) {
$panelVersion = "^$panelVersion";
}
}
$composerPackages = null;
// TODO: ask for composer packages?
// Create base directory
$this->filesystem->makeDirectory(plugin_path($id));
// Write plugin.json
$this->filesystem->put(plugin_path($id, 'plugin.json'), json_encode([
'id' => $id,
'name' => $name,
'author' => $author,
'version' => '1.0.0',
'description' => $description,
'category' => $category,
'url' => $url,
'update_url' => $updateUrl,
'namespace' => $namespace,
'class' => $class,
'panels' => $panels,
'panel_version' => $panelVersion,
'composer_packages' => $composerPackages,
'meta' => [
'status' => PluginStatus::Enabled,
'status_message' => null,
],
], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
// Create src directory and create main class
$this->filesystem->makeDirectory(plugin_path($id, 'src'));
$this->filesystem->put(plugin_path($id, 'src', $class . '.php'), Str::replace(['$namespace$', '$class$', '$id$'], [$namespace, $class, $id], file_get_contents(__DIR__ . '/Plugin.stub')));
// Create Providers directory and create service provider
$this->filesystem->makeDirectory(plugin_path($id, 'src', 'Providers'));
$this->filesystem->put(plugin_path($id, 'src', 'Providers', $class . 'Provider.php'), Str::replace(['$namespace$', '$class$'], [$namespace, $class], file_get_contents(__DIR__ . '/PluginProvider.stub')));
// Create config directory and create config file
$this->filesystem->makeDirectory(plugin_path($id, 'config'));
$this->filesystem->put(plugin_path($id, 'config', $id . '.php'), Str::replace(['$name$'], [$name], file_get_contents(__DIR__ . '/PluginConfig.stub')));
$this->info('Plugin created.');
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace $namespace$;
use Filament\Contracts\Plugin;
use Filament\Panel;
class $class$ implements Plugin
{
public function getId(): string
{
return '$id$';
}
public function register(Panel $panel): void
{
// Allows you to use any configuration option that is available to the panel.
// This includes registering resources, custom pages, themes, render hooks and more.
}
public function boot(Panel $panel): void
{
// Is run only when the panel that the plugin is being registered to is actually in-use. It is executed by a middleware class.
}
}

View File

@@ -1,5 +0,0 @@
<?php
return [
// Config values for $name$
];

View File

@@ -1,18 +0,0 @@
<?php
namespace $namespace$\Providers;
use Illuminate\Support\ServiceProvider;
class $class$Provider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
//
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class UninstallPluginCommand extends Command
{
protected $signature = 'p:plugin:uninstall {id?} {--delete : Delete the plugin files}';
protected $description = 'Uninstalls a plugin';
public function handle(PluginService $pluginService): void
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
if (!$plugin) {
$this->error('Plugin does not exist!');
return;
}
if ($plugin->status === PluginStatus::NotInstalled) {
$this->error('Plugin is not installed!');
return;
}
$deleteFiles = $this->option('delete');
if ($this->input->isInteractive() && !$deleteFiles) {
$deleteFiles = $this->confirm('Do you also want to delete the plugin files?');
}
try {
$pluginService->uninstallPlugin($plugin, $deleteFiles);
$this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.');
} catch (Exception $exception) {
$this->error('Could not uninstall plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class UpdatePluginCommand extends Command
{
protected $signature = 'p:plugin:update {id?}';
protected $description = 'Updates a plugin';
public function handle(PluginService $pluginService): void
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
if (!$plugin) {
$this->error('Plugin does not exist!');
return;
}
if (!$plugin->isUpdateAvailable()) {
$this->error("Plugin doesn't need updating!");
return;
}
try {
$pluginService->updatePlugin($plugin);
$this->info('Plugin updated.');
} catch (Exception $exception) {
$this->error('Could not update plugin: ' . $exception->getMessage());
}
}
}

View File

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

View File

@@ -2,13 +2,12 @@
namespace App\Contracts\Http;
use App\Enums\SubuserPermission;
interface ClientPermissionsRequest
{
/**
* Returns the permission used to validate that the authenticated user may perform
* this action against the given resource (server).
* Returns the permissions string indicating which permission should be used to
* validate that the authenticated user has permission to perform this action against
* the given resource (server).
*/
public function permission(): SubuserPermission|string;
public function permission(): string;
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Contracts\Plugins;
use Filament\Schemas\Components\Component;
interface HasPluginSettings
{
/**
* @return Component[]
*/
public function getSettingsForm(): array;
/**
* @param array<mixed, mixed> $data
*/
public function saveSettings(array $data): void;
}

View File

@@ -2,7 +2,6 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -13,12 +12,12 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel
case Successful = 'successful';
case Failed = 'failed';
public function getIcon(): BackedEnum
public function getIcon(): string
{
return match ($this) {
self::InProgress => TablerIcon::CircleDashed,
self::Successful => TablerIcon::CircleCheck,
self::Failed => TablerIcon::CircleX,
self::InProgress => 'tabler-circle-dashed',
self::Successful => 'tabler-circle-check',
self::Failed => 'tabler-circle-x',
};
}

View File

@@ -2,7 +2,6 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -24,20 +23,20 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
// HTTP Based
case Missing = 'missing';
public function getIcon(): BackedEnum
public function getIcon(): string
{
return match ($this) {
self::Created => TablerIcon::HeartPlus,
self::Starting => TablerIcon::HeartUp,
self::Running => TablerIcon::Heartbeat,
self::Restarting => TablerIcon::HeartBolt,
self::Exited => TablerIcon::HeartExclamation,
self::Paused => TablerIcon::HeartPause,
self::Dead, self::Offline => TablerIcon::HeartX,
self::Removing => TablerIcon::HeartDown,
self::Missing => TablerIcon::HeartSearch,
self::Stopping => TablerIcon::HeartMinus,
self::Created => 'tabler-heart-plus',
self::Starting => 'tabler-heart-up',
self::Running => 'tabler-heartbeat',
self::Restarting => 'tabler-heart-bolt',
self::Exited => 'tabler-heart-exclamation',
self::Paused => 'tabler-heart-pause',
self::Dead, self::Offline => 'tabler-heart-x',
self::Removing => 'tabler-heart-down',
self::Missing => 'tabler-heart-search',
self::Stopping => 'tabler-heart-minus',
};
}

View File

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

View File

@@ -11,8 +11,6 @@ enum CustomizationKey: string
case TopNavigation = 'top_navigation';
case DashboardLayout = 'dashboard_layout';
case ButtonStyle = 'button_style';
public function getDefaultValue(): string|int|bool
{
return match ($this) {
@@ -22,7 +20,6 @@ enum CustomizationKey: string
self::ConsoleGraphPeriod => 30,
self::TopNavigation => config('panel.filament.default-navigation', 'sidebar'),
self::DashboardLayout => 'grid',
self::ButtonStyle => true,
};
}

View File

@@ -1,141 +0,0 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
enum EditorLanguages: string implements HasLabel
{
case plaintext = 'plaintext';
case abap = 'abap';
case apex = 'apex';
case azcali = 'azcali';
case bat = 'bat';
case bicep = 'bicep';
case cameligo = 'cameligo';
case clojure = 'clojure';
case coffeescript = 'coffeescript';
case c = 'c';
case cpp = 'cpp';
case csharp = 'csharp';
case csp = 'csp';
case css = 'css';
case cypher = 'cypher';
case dart = 'dart';
case dockerfile = 'dockerfile';
case ecl = 'ecl';
case elixir = 'elixir';
case flow9 = 'flow9';
case fsharp = 'fsharp';
case go = 'go';
case graphql = 'graphql';
case handlebars = 'handlebars';
case hcl = 'hcl';
case html = 'html';
case ini = 'ini';
case java = 'java';
case javascript = 'javascript';
case julia = 'julia';
case json = 'json';
case kotlin = 'kotlin';
case less = 'less';
case lexon = 'lexon';
case lua = 'lua';
case liquid = 'liquid';
case m3 = 'm3';
case markdown = 'markdown';
case mdx = 'mdx';
case mips = 'mips';
case msdax = 'msdax';
case mysql = 'mysql';
case objectivec = 'objective-c';
case pascal = 'pascal';
case pascaligo = 'pascaligo';
case perl = 'perl';
case pgsql = 'pgsql';
case php = 'php';
case pla = 'pla';
case postiats = 'postiats';
case powerquery = 'powerquery';
case powershell = 'powershell';
case proto = 'proto';
case pug = 'pug';
case python = 'python';
case qsharp = 'qsharp';
case r = 'r';
case razor = 'razor';
case redis = 'redis';
case redshift = 'redshift';
case restructuredtext = 'restructuredtext';
case ruby = 'ruby';
case rust = 'rust';
case sb = 'sb';
case scala = 'scala';
case scheme = 'scheme';
case scss = 'scss';
case shell = 'shell';
case sol = 'sol';
case aes = 'aes';
case sparql = 'sparql';
case sql = 'sql';
case st = 'st';
case swift = 'swift';
case systemverilog = 'systemverilog';
case verilog = 'verilog';
case tcl = 'tcl';
case twig = 'twig';
case typescript = 'typescript';
case typespec = 'typespec';
case vb = 'vb';
case wgsl = 'wgsl';
case xml = 'xml';
case yaml = 'yaml';
public static function fromWithAlias(string $match): self
{
return match ($match) {
'h' => self::c,
'cc', 'hpp' => self::cpp,
'cs' => self::csharp,
'class' => self::java,
'htm' => self::html,
'js', 'mjs', 'cjs' => self::javascript,
'kt', 'kts' => self::kotlin,
'md' => self::markdown,
'm' => self::objectivec,
'pl', 'pm' => self::perl,
'php3', 'php4', 'php5', 'phtml' => self::php,
'py', 'pyc', 'pyo', 'pyi' => self::python,
'rdata', 'rds' => self::r,
'rb', 'erb' => self::ruby,
'sc' => self::scala,
'sh', 'zsh' => self::shell,
'ts', 'tsx' => self::typescript,
'yml' => self::yaml,
default => self::tryFrom($match) ?? self::plaintext,
};
}
public function getLabel(): string
{
return $this->name;
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum PluginCategory: string implements HasIcon, HasLabel
{
case Plugin = 'plugin';
case Theme = 'theme';
case Language = 'language';
public function getIcon(): BackedEnum
{
return match ($this) {
self::Plugin => TablerIcon::Package,
self::Theme => TablerIcon::Palette,
self::Language => TablerIcon::Language,
};
}
public function getLabel(): string
{
return trans('admin/plugin.category_enum.' . $this->value);
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum PluginStatus: string implements HasColor, HasIcon, HasLabel
{
case NotInstalled = 'not_installed';
case Disabled = 'disabled';
case Enabled = 'enabled';
case Errored = 'errored';
case Incompatible = 'incompatible';
public function getIcon(): BackedEnum
{
return match ($this) {
self::NotInstalled => TablerIcon::HeartOff,
self::Disabled => TablerIcon::HeartX,
self::Enabled => TablerIcon::HeartCheck,
self::Errored => TablerIcon::HeartBroken,
self::Incompatible => TablerIcon::HeartCancel,
};
}
public function getColor(): string
{
return match ($this) {
self::NotInstalled => 'gray',
self::Disabled => 'warning',
self::Enabled => 'success',
self::Errored => 'danger',
self::Incompatible => 'danger',
};
}
public function getLabel(): string
{
return trans('admin/plugin.status_enum.' . $this->value);
}
}

View File

@@ -1,65 +0,0 @@
<?php
namespace App\Enums;
use App\Models\Server;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\RateLimiter;
use Webmozart\Assert\Assert;
/**
* A basic resource throttler for individual servers. This is applied in addition
* to existing rate limits and allows the code to slow down speedy users that might
* be creating resources a little too quickly for comfort. This throttle generally
* only applies to creation flows, and not general view/edit/delete flows.
*/
enum ResourceLimit: string
{
case Websocket = 'websocket';
case AllocationCreate = 'allocation-create';
case BackupRestore = 'backup-restore';
case DatabaseCreate = 'database-create';
case ScheduleCreate = 'schedule-create';
case SubuserCreate = 'subuser-create';
case FilePull = 'file-pull';
public function throttleKey(): string
{
return "api.client:server-resource:{$this->name}";
}
/**
* Returns a middleware that will throttle the specific resource by server. This
* throttle applies to any user making changes to that resource on the specific
* server, it is NOT per-user.
*/
public function middleware(): string
{
return ThrottleRequests::using($this->throttleKey());
}
public function limit(): Limit
{
return match ($this) {
self::Websocket => Limit::perMinute(5),
self::BackupRestore => Limit::perMinutes(15, 3),
self::DatabaseCreate => Limit::perMinute(2),
self::SubuserCreate => Limit::perMinutes(15, 10),
self::FilePull => Limit::perMinutes(10, 5),
default => Limit::perMinute(2),
};
}
public static function boot(): void
{
foreach (self::cases() as $case) {
RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);
return $case->limit()->by($server->uuid);
});
}
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Enums;
enum RolePermissionModels: string
{
case ApiKey = 'apiKey';
case Allocation = 'allocation';
case DatabaseHost = 'databaseHost';
case Database = 'database';
case Egg = 'egg';
@@ -35,9 +34,4 @@ enum RolePermissionModels: string
{
return RolePermissionPrefixes::Update->value . ' ' . $this->value;
}
public function delete(): string
{
return RolePermissionPrefixes::Delete->value . ' ' . $this->value;
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -15,13 +14,14 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
case Suspended = 'suspended';
case RestoringBackup = 'restoring_backup';
public function getIcon(): BackedEnum
public function getIcon(): string
{
return match ($this) {
self::Installing => TablerIcon::HeartBolt,
self::InstallFailed, self::ReinstallFailed => TablerIcon::HeartX,
self::Suspended => TablerIcon::HeartCancel,
self::RestoringBackup => TablerIcon::HeartUp,
self::Installing => 'tabler-heart-bolt',
self::InstallFailed => 'tabler-heart-x',
self::ReinstallFailed => 'tabler-heart-x',
self::Suspended => 'tabler-heart-cancel',
self::RestoringBackup => 'tabler-heart-up',
};
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum StepPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

@@ -1,90 +0,0 @@
<?php
namespace App\Enums;
use BackedEnum;
enum SubuserPermission: string
{
case WebsocketConnect = 'websocket.connect';
case ControlConsole = 'control.console';
case ControlStart = 'control.start';
case ControlStop = 'control.stop';
case ControlRestart = 'control.restart';
case FileRead = 'file.read';
case FileReadContent = 'file.read-content';
case FileCreate = 'file.create';
case FileUpdate = 'file.update';
case FileDelete = 'file.delete';
case FileArchive = 'file.archive';
case FileSftp = 'file.sftp';
case BackupRead = 'backup.read';
case BackupCreate = 'backup.create';
case BackupDelete = 'backup.delete';
case BackupDownload = 'backup.download';
case BackupRestore = 'backup.restore';
case ScheduleRead = 'schedule.read';
case ScheduleCreate = 'schedule.create';
case ScheduleUpdate = 'schedule.update';
case ScheduleDelete = 'schedule.delete';
case UserRead = 'user.read';
case UserCreate = 'user.create';
case UserUpdate = 'user.update';
case UserDelete = 'user.delete';
case DatabaseRead = 'database.read';
case DatabaseCreate = 'database.create';
case DatabaseUpdate = 'database.update';
case DatabaseDelete = 'database.delete';
case DatabaseViewPassword = 'database.view-password';
case AllocationRead = 'allocation.read';
case AllocationCreate = 'allocation.create';
case AllocationUpdate = 'allocation.update';
case AllocationDelete = 'allocation.delete';
case ActivityRead = 'activity.read';
case StartupRead = 'startup.read';
case StartupUpdate = 'startup.update';
case StartupDockerImage = 'startup.docker-image';
case SettingsRename = 'settings.rename';
case SettingsDescription = 'settings.description';
case SettingsReinstall = 'settings.reinstall';
/** @return string[] */
public function split(): array
{
return explode('.', $this->value, 2);
}
public function isHidden(): bool
{
return $this === self::WebsocketConnect;
}
public function getIcon(): ?BackedEnum
{
[$group, $permission] = $this->split();
return match ($group) {
'control' => TablerIcon::Terminal2,
'user' => TablerIcon::Users,
'file' => TablerIcon::Files,
'backup' => TablerIcon::FileZip,
'allocation' => TablerIcon::Network,
'startup' => TablerIcon::PlayerPlay,
'database' => TablerIcon::Database,
'schedule' => TablerIcon::Clock,
'settings' => TablerIcon::Settings,
'activity' => TablerIcon::Stack,
default => null,
};
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum TabPosition: string
{
case Before = 'before';
case After = 'after';
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -25,11 +24,11 @@ enum WebhookType: string implements HasColor, HasIcon, HasLabel
};
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return match ($this) {
self::Regular => TablerIcon::WorldWww,
self::Discord => TablerIcon::BrandDiscord,
self::Regular => 'tabler-world-www',
self::Discord => 'tabler-brand-discord',
};
}
}

View File

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

View File

@@ -2,7 +2,6 @@
namespace App\Extensions\Captcha\Schemas;
use BackedEnum;
use Filament\Schemas\Components\Component;
interface CaptchaSchemaInterface
@@ -25,7 +24,7 @@ interface CaptchaSchemaInterface
*/
public function getSettingsForm(): array;
public function getIcon(): null|string|BackedEnum;
public function getIcon(): ?string;
public function validateResponse(?string $captchaResponse = null): void;
}

View File

@@ -2,10 +2,8 @@
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Enums\TablerIcon;
use App\Extensions\Captcha\Schemas\BaseSchema;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use BackedEnum;
use Exception;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
@@ -51,8 +49,8 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
->label(trans('admin/setting.captcha.verify'))
->columnSpan(2)
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
@@ -63,9 +61,9 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
]);
}
public function getIcon(): BackedEnum
public function getIcon(): ?string
{
return TablerIcon::BrandCloudflare;
return 'tabler-brand-cloudflare';
}
/**

View File

@@ -2,10 +2,9 @@
namespace App\Extensions\Features\Schemas;
use App\Enums\SubuserPermission;
use App\Enums\TablerIcon;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonServerRepository;
@@ -55,7 +54,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token')
->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupUpdate, $server))
->disabledSchema(fn () => !user()?->can(Permission::ACTION_STARTUP_UPDATE, $server))
->schema([
TextEntry::make('info')
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
@@ -74,7 +73,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
}
},
])
->hintIcon(TablerIcon::Code, fn () => implode('|', $serverVariable->variable->rules))
->hintIcon('tabler-code', fn () => implode('|', $serverVariable->variable->rules))
->label(fn () => $serverVariable->variable->name)
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),

View File

@@ -2,9 +2,9 @@
namespace App\Extensions\Features\Schemas;
use App\Enums\SubuserPermission;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
@@ -44,7 +44,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
->modalHeading('Unsupported Java Version')
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
->modalSubmitActionLabel('Update Docker Image')
->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupDockerImage, $server))
->disabledSchema(fn () => !user()?->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->schema([
TextEntry::make('java')
->label('Please select a supported version from the list below to continue starting the server.'),

View File

@@ -2,7 +2,6 @@
namespace App\Extensions\Features\Schemas;
use App\Enums\TablerIcon;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action;
use Illuminate\Support\Facades\Blade;
@@ -32,7 +31,7 @@ class PIDLimitSchema implements FeatureSchemaInterface
{
return Action::make($this->getId())
->requiresConfirmation()
->icon(TablerIcon::AlertTriangle)
->icon('tabler-alert-triangle')
->modalHeading(fn () => user()?->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML'

View File

@@ -2,18 +2,11 @@
namespace App\Extensions\Filesystem;
use Aws\CommandInterface;
use Aws\Result;
use Aws\S3\S3ClientInterface;
use GuzzleHttp\Client as GuzzleClient;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use RuntimeException;
use SimpleXMLElement;
class S3Filesystem extends AwsS3V3Adapter
{
private ?GuzzleClient $guzzle = null;
/**
* @param array<mixed> $options
*/
@@ -33,18 +26,6 @@ class S3Filesystem extends AwsS3V3Adapter
);
}
private function getGuzzleClient(): GuzzleClient
{
if ($this->guzzle === null) {
$this->guzzle = new GuzzleClient([
'timeout' => 30,
'connect_timeout' => 10,
]);
}
return $this->guzzle;
}
public function getClient(): S3ClientInterface
{
return $this->client;
@@ -54,78 +35,4 @@ class S3Filesystem extends AwsS3V3Adapter
{
return $this->bucket;
}
/**
* Execute an S3 command using a presigned URL for maximum compatibility
* with S3-compatible providers.
*
* @return Result<array<string, mixed>>
*/
public function executeS3Command(CommandInterface $command): Result
{
$presignedRequest = $this->client->createPresignedRequest($command, '+60 minutes');
$response = $this->getGuzzleClient()->send($presignedRequest);
$body = (string) $response->getBody();
$commandName = $command->getName();
// S3's CompleteMultipartUpload can return HTTP 200 with an <Error> body
if ($body !== '' && str_contains($body, '<Error>')) {
throw new RuntimeException("S3 returned an error for $commandName: $body");
}
return new Result($this->parseS3Response($commandName, $body));
}
/**
* Parse the XML response body based on the S3 command type.
*
* @return array<string, mixed>
*/
private function parseS3Response(string $commandName, string $body): array
{
if ($body === '') {
return [];
}
$xml = @simplexml_load_string($body);
if ($xml === false) {
throw new RuntimeException("Failed to parse S3 XML response for $commandName: $body");
}
return match ($commandName) {
'CreateMultipartUpload' => $this->parseCreateMultipartUpload($xml),
'ListParts' => $this->parseListParts($xml),
'CompleteMultipartUpload' => [],
default => [],
};
}
/**
* @return array{UploadId: string}
*/
private function parseCreateMultipartUpload(SimpleXMLElement $xml): array
{
return [
'UploadId' => (string) $xml->UploadId,
];
}
/**
* @return array{Parts: array<int, array{ETag: string, PartNumber: int}>}
*/
private function parseListParts(SimpleXMLElement $xml): array
{
$parts = [];
foreach ($xml->Part as $part) {
$parts[] = [
'ETag' => (string) $part->ETag,
'PartNumber' => (int) $part->PartNumber,
];
}
return ['Parts' => $parts];
}
}

View File

@@ -2,11 +2,8 @@
namespace App\Extensions\OAuth;
use App\Models\User;
use BackedEnum;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Wizard\Step;
use Laravel\Socialite\Contracts\User as OAuthUser;
interface OAuthSchemaInterface
{
@@ -30,13 +27,13 @@ interface OAuthSchemaInterface
/** @return Step[] */
public function getSetupSteps(): array;
public function getIcon(): null|string|BackedEnum;
public function getIcon(): ?string;
public function getHexColor(): ?string;
public function isEnabled(): bool;
public function shouldCreateMissingUser(OAuthUser $user): bool;
public function shouldCreateMissingUsers(): bool;
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool;
public function shouldLinkMissingUsers(): bool;
}

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -35,9 +33,9 @@ final class BitbucketSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandBitbucketFilled;
return 'tabler-brand-bitbucket-f';
}
public function getHexColor(): string

View File

@@ -2,15 +2,13 @@
namespace App\Extensions\OAuth\Schemas;
use BackedEnum;
final class CommonSchema extends OAuthSchema
{
public function __construct(
private readonly string $id,
private readonly ?string $name = null,
private readonly ?string $configName = null,
private readonly null|string|BackedEnum $icon = null,
private readonly ?string $icon = null,
private readonly ?string $hexColor = null,
) {}
@@ -29,7 +27,7 @@ final class CommonSchema extends OAuthSchema
return $this->configName ?? parent::getConfigKey();
}
public function getIcon(): null|string|BackedEnum
public function getIcon(): ?string
{
return $this->icon;
}

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -44,9 +42,9 @@ final class DiscordSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandDiscordFilled;
return 'tabler-brand-discord-f';
}
public function getHexColor(): string

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -38,9 +36,9 @@ final class FacebookSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandFacebookFilled;
return 'tabler-brand-facebook-f';
}
public function getHexColor(): string

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -20,11 +18,11 @@ final class GithubSchema extends OAuthSchema
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new GitHub OAuth App')
Step::make('Register new Github OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(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>'))),
->state(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>'))),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
@@ -44,9 +42,9 @@ final class GithubSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandGithubFilled;
return 'tabler-brand-github-f';
}
public function getHexColor(): string

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -55,9 +53,9 @@ final class GitlabSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandGitlab;
return 'tabler-brand-gitlab';
}
public function getHexColor(): string

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -44,9 +42,9 @@ final class GoogleSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandGoogleFilled;
return 'tabler-brand-google-f';
}
public function getHexColor(): string

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -35,9 +33,9 @@ final class LinkedinSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandLinkedinFilled;
return 'tabler-brand-linkedin-f';
}
public function getHexColor(): string

View File

@@ -2,17 +2,13 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use App\Extensions\OAuth\OAuthSchemaInterface;
use App\Models\User;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as OAuthUser;
abstract class OAuthSchema implements OAuthSchemaInterface
{
@@ -63,8 +59,8 @@ abstract class OAuthSchema implements OAuthSchemaInterface
->label(trans('admin/setting.oauth.create_missing_users'))
->columnSpan(2)
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
@@ -74,8 +70,8 @@ abstract class OAuthSchema implements OAuthSchemaInterface
->label(trans('admin/setting.oauth.link_missing_users'))
->columnSpan(2)
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
@@ -108,7 +104,7 @@ abstract class OAuthSchema implements OAuthSchemaInterface
return "OAUTH_{$id}_ENABLED";
}
public function getIcon(): null|string|BackedEnum
public function getIcon(): ?string
{
return null;
}
@@ -120,17 +116,19 @@ abstract class OAuthSchema implements OAuthSchemaInterface
public function isEnabled(): bool
{
return env($this->getConfigKey(), false);
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false);
}
public function shouldCreateMissingUser(OAuthUser $user): bool
public function shouldCreateMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool
public function shouldLinkMissingUsers(): bool
{
$id = Str::upper($this->getId());

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -35,9 +33,9 @@ final class SlackSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandSlack;
return 'tabler-brand-slack';
}
public function getHexColor(): string

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -61,9 +59,9 @@ final class SteamSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandSteamFilled;
return 'tabler-brand-steam-f';
}
public function getHexColor(): string

View File

@@ -2,8 +2,6 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -44,9 +42,9 @@ final class XSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
public function getIcon(): string
{
return TablerIcon::BrandX;
return 'tabler-brand-x';
}
public function getHexColor(): string

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,14 +2,12 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use App\Services\Helpers\SoftwareVersionService;
use BackedEnum;
use Filament\Pages\Dashboard as BaseDashboard;
class Dashboard extends BaseDashboard
{
protected static string|BackedEnum|null $navigationIcon = TablerIcon::LayoutDashboard;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-layout-dashboard';
private SoftwareVersionService $softwareVersionService;

View File

@@ -2,8 +2,6 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use BackedEnum;
use Carbon\Carbon;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
@@ -15,7 +13,7 @@ use Spatie\Health\ResultStores\ResultStore;
class Health extends Page
{
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Heart;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-heart';
protected string $view = 'filament.pages.health';
@@ -48,9 +46,8 @@ class Health extends Page
{
return [
Action::make('refresh')
->hiddenLabel()
->tooltip(trans('admin/health.refresh'))
->icon(TablerIcon::Refresh)
->label(trans('admin/health.refresh'))
->button()
->action('refresh'),
];
}
@@ -129,16 +126,16 @@ class Health extends Page
return trans('admin/health.checks.failed', ['checks' => implode(', ', $failedNames)]);
}
public static function getNavigationIcon(): BackedEnum
public static function getNavigationIcon(): string
{
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
$results = app(ResultStore::class)->latestResults();
if ($results === null) {
return TablerIcon::HeartQuestion;
return 'tabler-heart-question';
}
return $results->containsFailingCheck() ? TablerIcon::HeartExclamation : TablerIcon::HeartCheck;
return $results->containsFailingCheck() ? 'tabler-heart-exclamation' : 'tabler-heart-check';
}
public function backgroundColor(string $str): string
@@ -163,14 +160,14 @@ class Health extends Page
};
}
public function icon(string $str): BackedEnum
public function icon(string $str): string
{
return match ($str) {
Status::ok()->value => TablerIcon::CircleCheck,
Status::warning()->value => TablerIcon::ExclamationCircle,
Status::skipped()->value => TablerIcon::CircleChevronRight,
Status::failed()->value, Status::crashed()->value => TablerIcon::CircleX,
default => TablerIcon::HelpCircle
Status::ok()->value => 'tabler-circle-check',
Status::warning()->value => 'tabler-exclamation-circle',
Status::skipped()->value => 'tabler-circle-chevron-right',
Status::failed()->value, Status::crashed()->value => 'tabler-circle-x',
default => 'tabler-help-circle'
};
}
}

View File

@@ -2,17 +2,16 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Actions\ViewLogAction;
use Boquizo\FilamentLogViewer\Pages\ListLogs as BaseListLogs;
use Boquizo\FilamentLogViewer\Tables\Columns\LevelColumn;
use Boquizo\FilamentLogViewer\Tables\Columns\NameColumn;
use Boquizo\FilamentLogViewer\UseCases\ParseDateUseCase;
use Boquizo\FilamentLogViewer\Utils\Level;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Http;
@@ -29,7 +28,7 @@ class ListLogs extends BaseListLogs
{
return parent::table($table)
->emptyStateHeading(trans('admin/log.empty_table'))
->emptyStateIcon(TablerIcon::Check)
->emptyStateIcon('tabler-check')
->columns([
NameColumn::make('date'),
LevelColumn::make(Level::ALL)
@@ -47,21 +46,18 @@ class ListLogs extends BaseListLogs
])
->recordActions([
ViewLogAction::make()
->icon(TablerIcon::FileDescription)->iconButton(),
->icon('tabler-file-description')->iconSize(IconSize::Medium),
DownloadAction::make()
->tooltip(fn ($record) => trans('filament-log-viewer::log.table.actions.download.label', ['log' => ParseDateUseCase::execute($record['date'])]))
->icon(TablerIcon::FileDownload)->iconButton(),
->icon('tabler-file-download')->iconSize(IconSize::Medium),
Action::make('uploadLogs')
->button()
->hiddenLabel()
->tooltip(trans('admin/log.actions.upload_tooltip', ['url' => 'logs.pelican.dev']))
->icon(TablerIcon::WorldUpload)
->icon('tabler-world-upload')->iconSize(IconSize::Medium)
->requiresConfirmation()
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn ($record) => trans('admin/log.actions.upload_logs_description', ['file' => $record['date'], 'url' => 'https://logs.pelican.dev']))
->action(function ($record) {
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
$extension = config('filament-log-viewer.pattern.extension', '.log');
$logPath = storage_path('logs/' . $prefix . $record['date'] . $extension);
$logPath = storage_path('logs/' . $record['date']);
if (!file_exists($logPath)) {
Notification::make()
@@ -78,12 +74,18 @@ class ListLogs extends BaseListLogs
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)
->asMultipart()
->attach('c', $content)
->attach('e', '14d')
->post('https://logs.pelican.dev');
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
@@ -103,7 +105,7 @@ class ListLogs extends BaseListLogs
->body("{$url}")
->success()
->actions([
Action::make('exclude_viewLogs')
Action::make('viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
@@ -122,7 +124,7 @@ class ListLogs extends BaseListLogs
}
}),
DeleteAction::make()
->icon(TablerIcon::Trash)->iconButton(),
->icon('tabler-trash')->iconSize(IconSize::Medium),
]);
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use App\Extensions\Avatar\AvatarService;
use App\Extensions\Captcha\CaptchaService;
use App\Extensions\OAuth\OAuthService;
@@ -11,7 +10,6 @@ use App\Notifications\MailTested;
use App\Traits\EnvironmentWriterTrait;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
@@ -54,11 +52,10 @@ class Settings extends Page implements HasSchemas
CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions;
}
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
use EnvironmentWriterTrait;
use InteractsWithForms;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Settings;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-settings';
protected string $view = 'filament.pages.settings';
@@ -98,7 +95,11 @@ class Settings extends Page implements HasSchemas
return trans('admin/setting.title');
}
/** @return array<Component> */
/**
* @return array<Component>
*
* @throws Exception
*/
protected function getFormSchema(): array
{
return [
@@ -106,44 +107,34 @@ class Settings extends Page implements HasSchemas
->columns()
->persistTabInQueryString()
->disabled(fn () => !user()?->can('update settings'))
->tabs($this->getTabs()),
];
}
/**
* @return Tab[]
*
* @throws Exception
*/
protected function getDefaultTabs(): array
{
return [
Tab::make('general')
->label(trans('admin/setting.navigation.general'))
->icon(TablerIcon::Home)
->schema($this->generalSettings()),
Tab::make('captcha')
->label(trans('admin/setting.navigation.captcha'))
->icon(TablerIcon::Shield)
->schema($this->captchaSettings())
->columns(1),
Tab::make('mail')
->label(trans('admin/setting.navigation.mail'))
->icon(TablerIcon::Mail)
->schema($this->mailSettings()),
Tab::make('backup')
->label(trans('admin/setting.navigation.backup'))
->icon(TablerIcon::Box)
->schema($this->backupSettings()),
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon(TablerIcon::BrandOauth)
->schema($this->oauthSettings())
->columns(1),
Tab::make('misc')
->label(trans('admin/setting.navigation.misc'))
->icon(TablerIcon::Tool)
->schema($this->miscSettings()),
->tabs([
Tab::make('general')
->label(trans('admin/setting.navigation.general'))
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('captcha')
->label(trans('admin/setting.navigation.captcha'))
->icon('tabler-shield')
->schema($this->captchaSettings())
->columns(1),
Tab::make('mail')
->label(trans('admin/setting.navigation.mail'))
->icon('tabler-mail')
->schema($this->mailSettings()),
Tab::make('backup')
->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth')
->schema($this->oauthSettings())
->columns(1),
Tab::make('misc')
->label(trans('admin/setting.navigation.misc'))
->icon('tabler-tool')
->schema($this->miscSettings()),
]),
];
}
@@ -162,12 +153,12 @@ class Settings extends Page implements HasSchemas
->schema([
TextInput::make('APP_LOGO')
->label(trans('admin/setting.general.app_logo'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/setting.general.app_logo_help'))
->hintIcon('tabler-question-mark', 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(TablerIcon::QuestionMark, trans('admin/setting.general.app_favicon_help'))
->hintIcon('tabler-question-mark', trans('admin/setting.general.app_favicon_help'))
->required()
->default(env('APP_FAVICON', '/pelican.ico'))
->placeholder('/pelican.ico'),
@@ -178,8 +169,8 @@ class Settings extends Page implements HasSchemas
Toggle::make('APP_DEBUG')
->label(trans('admin/setting.general.debug_mode'))
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->stateCast(new BooleanStateCast(false))
@@ -196,8 +187,8 @@ class Settings extends Page implements HasSchemas
Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
->label(trans('admin/setting.general.uploadable_avatars'))
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->stateCast(new BooleanStateCast(false))
@@ -244,16 +235,16 @@ class Settings extends Page implements HasSchemas
->placeholder(trans('admin/setting.general.trusted_proxies_help'))
->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies')))))
->hintActions([
Action::make('hint_clear')
Action::make('clear')
->label(trans('admin/setting.general.clear'))
->color('danger')
->icon(TablerIcon::Trash)
->icon('tabler-trash')
->requiresConfirmation()
->authorize(fn () => user()?->can('update settings'))
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
Action::make('hint_cloudflare')
Action::make('cloudflare')
->label(trans('admin/setting.general.set_to_cf'))
->icon(TablerIcon::BrandCloudflare)
->icon('tabler-brand-cloudflare')
->authorize(fn () => user()?->can('update settings'))
->action(function (Factory $client, Set $set) {
$ips = collect();
@@ -264,7 +255,7 @@ class Settings extends Page implements HasSchemas
->connectTimeout(3)
->get('https://api.cloudflare.com/client/v4/ips');
if ($response->status() === 200) {
if ($response->getStatusCode() === 200) {
$result = $response->json('result');
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
$ips->push(...data_get($result, $value));
@@ -295,7 +286,7 @@ class Settings extends Page implements HasSchemas
$formFields[] = Section::make($schema->getName())
->columns(5)
->icon($schema->getIcon() ?? TablerIcon::Shield)
->icon($schema->getIcon() ?? 'tabler-shield')
->collapsed(fn () => !$schema->isEnabled())
->collapsible()
->schema([
@@ -305,13 +296,11 @@ class Settings extends Page implements HasSchemas
Actions::make([
Action::make("disable_captcha_$id")
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.captcha.disable'))
->color('danger')
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", false)),
Action::make("enable_captcha_$id")
->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED"))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.captcha.enable'))
->color('success')
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", true)),
@@ -349,9 +338,9 @@ class Settings extends Page implements HasSchemas
->live()
->default(env('MAIL_MAILER', config('mail.default')))
->hintAction(
Action::make('hint_test')
Action::make('test')
->label(trans('admin/setting.mail.test_mail'))
->icon(TablerIcon::Send)
->icon('tabler-send')
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
->authorize(fn () => user()?->can('update settings'))
->action(function (Get $get) {
@@ -541,8 +530,8 @@ class Settings extends Page implements HasSchemas
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
->label(trans('admin/setting.backup.s3.use_path_style_endpoint'))
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
@@ -563,27 +552,26 @@ class Settings extends Page implements HasSchemas
$oauthSchemas = $this->oauthService->getAll();
foreach ($oauthSchemas as $schema) {
$id = Str::upper($schema->getId());
$key = $schema->getConfigKey();
$formFields[] = Section::make($schema->getName())
->columns(5)
->icon($schema->getIcon() ?? TablerIcon::BrandOauth)
->collapsed(fn () => !$schema->isEnabled())
->icon($schema->getIcon() ?? 'tabler-brand-oauth')
->collapsed(fn () => !env($key, false))
->collapsible()
->schema([
Hidden::make($key)
->live()
->default($schema->isEnabled()),
->default(env($key)),
Actions::make([
Action::make('disable_oauth_' . $schema->getId())
Action::make("disable_oauth_$id")
->visible(fn (Get $get) => $get($key))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.oauth.disable'))
->color('danger')
->action(fn (Set $set) => $set($key, false)),
Action::make('enable_oauth_' . $schema->getId())
Action::make("enable_oauth_$id")
->visible(fn (Get $get) => !$get($key))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.oauth.enable'))
->color('success')
->steps($schema->getSetupSteps())
@@ -626,26 +614,14 @@ class Settings extends Page implements HasSchemas
->schema([
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
->label(trans('admin/setting.misc.auto_allocation.question'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->stateCast(new BooleanStateCast(false))
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
Toggle::make('PANEL_CLIENT_ALLOCATIONS_CREATE_NEW')
->label(trans('admin/setting.misc.auto_allocation.create_new'))
->helperText(trans('admin/setting.misc.auto_allocation.create_new_help'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
->stateCast(new BooleanStateCast(false))
->default(env('PANEL_CLIENT_ALLOCATIONS_CREATE_NEW', config('panel.client_features.allocations.create_new'))),
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
->label(trans('admin/setting.misc.auto_allocation.start'))
->required()
@@ -671,8 +647,8 @@ class Settings extends Page implements HasSchemas
->schema([
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.server_installed'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
@@ -681,8 +657,8 @@ class Settings extends Page implements HasSchemas
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.server_reinstalled'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
@@ -730,8 +706,8 @@ class Settings extends Page implements HasSchemas
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
->label(trans('admin/setting.misc.activity_log.log_admin'))
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
@@ -767,8 +743,8 @@ class Settings extends Page implements HasSchemas
->schema([
Toggle::make('PANEL_EDITABLE_SERVER_DESCRIPTIONS')
->label(trans('admin/setting.misc.server.edit_server_desc'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
@@ -780,7 +756,6 @@ class Settings extends Page implements HasSchemas
->hint(trans('admin/setting.misc.server.console_font_hint'))
->label(trans('admin/setting.misc.server.console_font_upload'))
->directory('fonts')
->disk('public')
->columnSpan(1)
->maxFiles(1)
->preserveFilenames(),
@@ -830,6 +805,7 @@ class Settings extends Page implements HasSchemas
$this->writeToEnvironment($data);
Artisan::call('config:clear');
Artisan::call('queue:restart');
$this->redirect($this->getUrl());
@@ -852,10 +828,7 @@ class Settings extends Page implements HasSchemas
{
return [
Action::make('save')
->hiddenLabel()
->icon(TablerIcon::DeviceFloppy)
->action('save')
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->authorize(fn () => user()?->can('update settings'))
->keyBindings(['mod+s']),
];

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use App\Traits\ResolvesRecordDate;
use Boquizo\FilamentLogViewer\Actions\BackAction;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
@@ -10,6 +9,7 @@ use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Pages\ViewLog as BaseViewLog;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Http;
class ViewLogs extends BaseViewLog
@@ -19,24 +19,20 @@ class ViewLogs extends BaseViewLog
public function getHeaderActions(): array
{
return [
BackAction::make()
->tooltip(trans('filament-log-viewer::log.table.actions.close.label'))
->icon(TablerIcon::ArrowLeft)->iconButton(),
DeleteAction::make(withTooltip: true)
->icon(TablerIcon::Trash)->iconButton(),
->icon('tabler-trash')->iconSize(IconSize::Medium),
DownloadAction::make(withTooltip: true)
->icon(TablerIcon::FileDownload)->iconButton(),
->icon('tabler-file-download')->iconSize(IconSize::Medium),
Action::make('uploadLogs')
->button()
->hiddenLabel()
->icon(TablerIcon::WorldUpload)
->icon('tabler-world-upload')->iconSize(IconSize::Medium)
->requiresConfirmation()
->tooltip(trans('admin/log.actions.upload_tooltip', ['url' => 'logs.pelican.dev']))
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn () => trans('admin/log.actions.upload_logs_description', ['file' => $this->resolveRecordDate(), 'url' => 'https://logs.pelican.dev']))
->action(function () {
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
$extension = config('filament-log-viewer.pattern.extension', '.log');
$logPath = storage_path('logs/' . $prefix . $this->resolveRecordDate() . $extension);
$logPath = storage_path('logs/' . $this->resolveRecordDate());
if (!file_exists($logPath)) {
Notification::make()
@@ -53,12 +49,18 @@ class ViewLogs extends BaseViewLog
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)
->asMultipart()
->attach('c', $content)
->attach('e', '14d')
->post('https://logs.pelican.dev');
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
@@ -78,7 +80,7 @@ class ViewLogs extends BaseViewLog
->body("{$url}")
->success()
->actions([
Action::make('exclude_viewLogs')
Action::make('viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
@@ -96,6 +98,8 @@ class ViewLogs extends BaseViewLog
return;
}
}),
BackAction::make()
->icon('tabler-arrow-left')->iconSize(IconSize::Medium),
];
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\ApiKeys;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\ApiKeys\Pages\CreateApiKey;
use App\Filament\Admin\Resources\ApiKeys\Pages\ListApiKeys;
use App\Filament\Admin\Resources\Users\Pages\EditUser;
@@ -12,7 +11,6 @@ use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
@@ -22,7 +20,6 @@ use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -37,7 +34,7 @@ class ApiKeyResource extends Resource
protected static ?string $model = ApiKey::class;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Key;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-key';
public static function getNavigationLabel(): string
{
@@ -80,7 +77,7 @@ class ApiKeyResource extends Resource
->columns([
TextColumn::make('key')
->label(trans('admin/apikey.table.key'))
->icon(TablerIcon::ClipboardText)
->icon('tabler-clipboard-text')
->state(fn (ApiKey $key) => $key->identifier . $key->token)
->copyable(),
TextColumn::make('memo')
@@ -101,12 +98,12 @@ class ApiKeyResource extends Resource
->recordActions([
DeleteAction::make(),
])
->toolbarActions([
CreateAction::make(),
])
->emptyStateIcon(TablerIcon::Key)
->emptyStateIcon('tabler-key')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/apikey.empty'));
->emptyStateHeading(trans('admin/apikey.empty'))
->emptyStateActions([
CreateAction::make(),
]);
}
/**
@@ -114,44 +111,12 @@ class ApiKeyResource extends Resource
*/
public static function defaultForm(Schema $schema): Schema
{
$permissionList = ApiKey::getPermissionList();
return $schema
->components([
Section::make(trans('admin/apikey.permissions.all'))
->description(trans('admin/apikey.permissions.all_description'))
->columnSpanFull()
->schema([
ToggleButtons::make('permissions_all')
->hiddenLabel()
->inline()
->options([
0 => trans('admin/apikey.permissions.none'),
1 => trans('admin/apikey.permissions.read'),
3 => trans('admin/apikey.permissions.read_write'),
])
->icons([
0 => TablerIcon::BookOff,
1 => TablerIcon::Book,
3 => TablerIcon::Writing,
])
->colors([
0 => 'success',
1 => 'warning',
3 => 'danger',
])
->live()
->afterStateUpdated(function ($state, callable $set) use ($permissionList) {
foreach ($permissionList as $resource) {
$set('permissions_' . $resource, $state);
}
})
->default(0),
]),
Fieldset::make('Permissions')
->columnSpanFull()
->schema(
collect($permissionList)->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
->label(str($resource)->replace('_', ' ')->title())->inline()
->options([
0 => trans('admin/apikey.permissions.none'),
@@ -159,9 +124,9 @@ class ApiKeyResource extends Resource
3 => trans('admin/apikey.permissions.read_write'),
])
->icons([
0 => TablerIcon::BookOff,
1 => TablerIcon::Book,
3 => TablerIcon::Writing,
0 => 'tabler-book-off',
1 => 'tabler-book',
3 => 'tabler-writing',
])
->colors([
0 => 'success',

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\ApiKeys\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\ApiKeys\ApiKeyResource;
use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions;
@@ -26,12 +25,7 @@ class CreateApiKey extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
Action::make('create')
->hiddenLabel()
->action('create')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->icon(TablerIcon::FilePlus),
$this->getCreateFormAction()->formId('form'),
];
}

View File

@@ -3,8 +3,12 @@
namespace App\Filament\Admin\Resources\ApiKeys\Pages;
use App\Filament\Admin\Resources\ApiKeys\ApiKeyResource;
use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListApiKeys extends ListRecords
@@ -13,4 +17,13 @@ class ListApiKeys extends ListRecords
use CanCustomizeHeaderWidgets;
protected static string $resource = ApiKeyResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->hidden(fn () => ApiKey::where('key_type', ApiKey::TYPE_APPLICATION)->count() <= 0),
];
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\DatabaseHosts;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\CreateDatabaseHost;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\EditDatabaseHost;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\ListDatabaseHosts;
@@ -13,9 +12,7 @@ use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
@@ -41,7 +38,7 @@ class DatabaseHostResource extends Resource
protected static ?string $model = DatabaseHost::class;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Database;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-database';
protected static ?string $recordTitleAttribute = 'name';
@@ -95,18 +92,18 @@ class DatabaseHostResource extends Resource
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
->hidden(fn ($record) => static::canEdit($record)),
EditAction::make(),
])
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
->groupedBulkActions([
DeleteBulkAction::make(),
])
->emptyStateIcon(TablerIcon::Database)
->emptyStateIcon('tabler-database')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/databasehost.no_database_hosts'));
->emptyStateHeading(trans('admin/databasehost.no_database_hosts'))
->emptyStateActions([
CreateAction::make(),
]);
}
/**

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
@@ -176,7 +175,7 @@ class CreateDatabaseHost extends CreateRecord
->title(trans('admin/databasehost.error'))
->body($exception->getMessage())
->color('danger')
->icon(TablerIcon::Database)
->icon('tabler-database')
->danger()
->send();

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Models\DatabaseHost;
use App\Services\Databases\Hosts\HostUpdateService;
@@ -36,14 +35,9 @@ class EditDatabaseHost extends EditRecord
{
return [
DeleteAction::make()
->tooltip(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? trans('admin/databasehost.delete_help') : trans('filament-actions::delete.single.modal.actions.delete.label'))
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? trans('admin/databasehost.delete_help') : trans('filament-actions::delete.single.modal.actions.delete.label'))
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
$this->getSaveFormAction()->formId('form'),
];
}
@@ -65,7 +59,7 @@ class EditDatabaseHost extends EditRecord
->title(trans('admin/databasehost.error'))
->body($exception->getMessage())
->color('danger')
->icon(TablerIcon::Database)
->icon('tabler-database')
->danger()
->send();

View File

@@ -3,8 +3,12 @@
namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Models\DatabaseHost;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListDatabaseHosts extends ListRecords
@@ -13,4 +17,13 @@ class ListDatabaseHosts extends ListRecords
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseHostResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->hidden(fn () => DatabaseHost::count() <= 0),
];
}
}

View File

@@ -36,7 +36,7 @@ class DatabasesRelationManager extends RelationManager
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
TextInput::make('max_connections')
->label(trans('admin/databasehost.table.max_connections'))
->formatStateUsing(fn (Database $record) => $record->max_connections ?: trans('admin/databasehost.unlimited')),
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
TextInput::make('jdbc')
->label(trans('admin/databasehost.table.connection_string'))
->columnSpanFull()
@@ -62,7 +62,7 @@ class DatabasesRelationManager extends RelationManager
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
TextColumn::make('max_connections')
->label(trans('admin/databasehost.table.max_connections'))
->formatStateUsing(fn ($record) => $record->max_connections ?: trans('server/database.unlimited')),
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
DateTimeColumn::make('created_at')
->label(trans('admin/databasehost.table.created_at')),
])

View File

@@ -3,7 +3,6 @@
namespace App\Filament\Admin\Resources\Eggs;
use App\Enums\CustomizationKey;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\Pages\CreateEgg;
use App\Filament\Admin\Resources\Eggs\Pages\EditEgg;
use App\Filament\Admin\Resources\Eggs\Pages\ListEggs;
@@ -11,7 +10,6 @@ use App\Filament\Admin\Resources\Eggs\RelationManagers\ServersRelationManager;
use App\Models\Egg;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use BackedEnum;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
@@ -23,7 +21,7 @@ class EggResource extends Resource
protected static ?string $model = Egg::class;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Eggs;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-eggs';
protected static ?string $recordTitleAttribute = 'name';

View File

@@ -2,18 +2,16 @@
namespace App\Filament\Admin\Resources\Eggs\Pages;
use App\Enums\EditorLanguages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Filament\Components\Forms\Fields\MonacoEditor;
use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
@@ -37,7 +35,6 @@ class CreateEgg extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
protected static string $resource = EggResource::class;
@@ -47,12 +44,7 @@ class CreateEgg extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
Action::make('create')
->hiddenLabel()
->action('create')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->icon(TablerIcon::FilePlus),
$this->getCreateFormAction()->formId('form'),
];
}
@@ -61,231 +53,226 @@ class CreateEgg extends CreateRecord
return [];
}
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make()
->tabs($this->getTabs())
->columnSpanFull()
->persistTabInQueryString(),
]);
}
/** @return Tab[] */
protected function getDefaultTabs(): array
{
return [
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->maxLength(255)
->required()
->email()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.author_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(2)
->columnSpanFull()
->helperText(trans('admin/egg.description_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->keyPlaceholder('Default')
->valueLabel(trans('admin/egg.startup_command'))
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->label(trans('admin/egg.force_ip'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->default(1),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->keyPlaceholder('Java 21')
->valueLabel(trans('admin/egg.docker_uri'))
->valuePlaceholder('ghcr.io/pelican-eggs/yolks:java_21')
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->required()
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->default('{}')
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->default('{}')
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
->hiddenLabel()
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->reorderable()->orderColumn()
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->maxLength(255)
->required()
->email()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.author_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(2)
->columnSpanFull()
->helperText(trans('admin/egg.description_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->keyPlaceholder('Default')
->valueLabel(trans('admin/egg.startup_command'))
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->default(1),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->keyPlaceholder('Java 21')
->valueLabel(trans('admin/egg.docker_uri'))
->valuePlaceholder('ghcr.io/parkervcp/yolks:java_21')
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->required()
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon(TablerIcon::Code, fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->default('{}')
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->default('{}')
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
->hiddenLabel()
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->reorderable()->orderColumn()
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
TextInput::make('name')
->label(trans('admin/egg.name'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
]),
]),
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->default('bash')
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))
->language(EditorLanguages::shell)
->columnSpanFull()
->lazy(),
]),
];
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->default('bash')
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
CodeEditor::make('script_install')
->label(trans('admin/egg.script_install'))
->columnSpanFull()
->lazy(),
]),
])->columnSpanFull()->persistTabInQueryString(),
]);
}
protected function handleRecordCreation(array $data): Model

View File

@@ -2,23 +2,20 @@
namespace App\Filament\Admin\Resources\Eggs\Pages;
use App\Enums\EditorLanguages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction;
use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Filament\Components\Forms\Fields\MonacoEditor;
use App\Models\Egg;
use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
@@ -41,407 +38,411 @@ use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\Unique;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class EditEgg extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
protected static string $resource = EggResource::class;
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make()
->tabs($this->getTabs())
->columnSpanFull()
->persistTabInQueryString(),
]);
}
/** @return Tab[] */
protected function getDefaultTabs(): array
{
return [
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon(TablerIcon::Egg)
->schema([
Grid::make(2)
->columnSpan(1)
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon('tabler-egg')
->schema([
Image::make('', '')
->hidden(fn ($record) => !$record->image)
->url(fn ($record) => $record->image)
->alt('')
->alignJustify()
->imageSize(150)
->columnSpanFull(),
Flex::make([
Action::make('uploadImage')
->hiddenLabel()
->tooltip(trans('admin/egg.import.import_image'))
->iconSize(IconSize::Large)
->icon(TablerIcon::PhotoUp)
->modal()
->modalHeading('')
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('imageUrl'),
Hidden::make('imageExtension'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
$set('imageUrl', null);
$set('imageExtension', null);
Grid::make(2)
->columnSpan(1)
->schema([
Image::make('', '')
->hidden(fn ($record) => !$record->image)
->url(fn ($record) => $record->image)
->alt('')
->alignJustify()
->imageSize(150)
->columnSpanFull(),
Flex::make([
Action::make('uploadImage')
->iconButton()
->iconSize(IconSize::Large)
->icon('tabler-photo-up')
->modal()
->modalHeading('')
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('base64Image'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
return;
}
return;
}
try {
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
try {
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
$allowedExtensions = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
];
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
}
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (!array_key_exists($extension, $allowedExtensions)) {
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', $allowedExtensions)]));
}
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new Exception(trans('admin/egg.import.no_local_ip'));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
$set('imageUrl', $state);
$set('imageExtension', $extension);
$set('image_url_error', null);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new \Exception(trans('admin/egg.import.no_local_ip'));
}
} catch (Exception $e) {
$set('image_url_error', $e->getMessage());
$set('imageUrl', null);
$set('imageExtension', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn ($get) => $get('image_url_error') !== null)
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$imageContent = @file_get_contents($state, false, $context, 0, 1048576); // 1024KB
if (!$imageContent) {
throw new \Exception(trans('admin/egg.import.image_error'));
}
if (strlen($imageContent) >= 1048576) {
throw new \Exception(trans('admin/egg.import.image_too_large'));
}
$mimeType = $allowedExtensions[$extension];
$base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent);
$set('base64Image', $base64);
$set('image_url_error', null);
} catch (\Exception $e) {
$set('image_url_error', $e->getMessage());
$set('base64Image', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn ($get) => $get('image_url_error') !== null)
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(1024)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->saveUploadedFileUsing(function ($file, Set $set) {
$base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath()));
$set('base64Image', $base64);
return $base64;
}),
]),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->disk('public')
->directory(Egg::ICON_STORAGE_PATH)
->acceptedFileTypes([
'image/png',
'image/jpeg',
'image/webp',
'image/svg+xml',
])
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
return $record->uuid . '.' . $file->getClientOriginalExtension();
}),
]),
]),
])
->action(function (array $data, $record): void {
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
])
->action(function (array $data, $record): void {
$base64 = $data['base64Image'] ?? null;
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
if (empty($base64) && !empty($data['image'])) {
$base64 = $data['image'];
}
return;
}
if (!empty($base64)) {
$record->update([
'image' => $base64,
]);
if (!empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
$record->refresh();
} else {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('deleteImage')
->visible(fn ($record) => $record->image)
->label('')
->icon('tabler-trash')
->iconButton()
->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
if (empty($data['imageUrl']) && empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('delete_image')
->visible(fn ($record) => $record->image)
->hiddenLabel()
->tooltip(trans('admin/egg.import.delete_image'))
->icon(TablerIcon::Trash)
->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
$record->update([
'image' => null,
]);
Notification::make()
->title(trans('admin/egg.import.image_deleted'))
->success()
->send();
Notification::make()
->title(trans('admin/egg.import.image_deleted'))
->success()
->send();
$record->refresh();
}),
]),
]),
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
->helperText(trans('admin/egg.description_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->columnSpan(1)
->disabled(),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->valueLabel(trans('admin/egg.startup_command'))
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.update_url_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon(TablerIcon::ServerCog)
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon(TablerIcon::Variable)
->schema([
Repeater::make('variables')
->hiddenLabel()
->grid()
->relationship('variables')
->reorderable()
->collapsible()->collapsed()
->orderColumn()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
$record->refresh();
}),
]),
]),
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
->helperText(trans('admin/egg.description_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->columnSpan(1)
->disabled(),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon(TablerIcon::Code, fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->valueLabel(trans('admin/egg.startup_command'))
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon('tabler-server-cog')
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon('tabler-variable')
->schema([
Repeater::make('variables')
->hiddenLabel()
->grid()
->relationship('variables')
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
->collapsible()->collapsed()
->orderColumn()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
]),
]),
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon(TablerIcon::FileDownload)
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->hiddenLabel()
->language(EditorLanguages::shell)
->columnSpanFull(),
]),
];
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon('tabler-file-download')
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
CodeEditor::make('script_install')
->hiddenLabel()
->columnSpanFull(),
]),
])->columnSpanFull()->persistTabInQueryString(),
]);
}
/** @return array<Action|ActionGroup> */
@@ -450,16 +451,11 @@ class EditEgg extends EditRecord
return [
DeleteAction::make()
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->tooltip(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')),
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')),
ExportEggAction::make(),
ImportEggAction::make()
->multiple(false),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
$this->getSaveFormAction()->formId('form'),
];
}
@@ -468,37 +464,6 @@ class EditEgg extends EditRecord
$this->fillForm();
}
/**
* Save an image from URL download to a file.
*
* @throws Exception
*/
private function saveImageFromUrl(string $imageUrl, string $extension, Egg $egg): void
{
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$data = @file_get_contents($imageUrl, false, $context, 0, 1048576); // 1024KB
if (empty($data)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
}
protected function getFormActions(): array
{
return [];

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\Eggs\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction;
@@ -13,17 +12,16 @@ use App\Models\Egg;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ReplicateAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class ListEggs extends ListRecords
@@ -64,12 +62,16 @@ class ListEggs extends ListRecords
])
->recordActions([
EditAction::make()
->iconButton()
->tooltip(trans('filament-actions::edit.single.label')),
ExportEggAction::make()
->iconButton()
->tooltip(trans('filament-actions::export.modal.actions.export.label')),
UpdateEggAction::make()
->iconButton()
->tooltip(trans_choice('admin/egg.update', 1)),
ReplicateAction::make()
->iconButton()
->tooltip(trans('filament-actions::replicate.single.label'))
->modal(false)
->excludeAttributes(['author', 'uuid', 'update_url', 'servers_count', 'created_at', 'updated_at'])
@@ -81,59 +83,41 @@ class ListEggs extends ListRecords
->after(fn (Egg $record, Egg $replica) => $record->variables->each(fn ($variable) => $variable->replicate()->fill(['egg_id' => $replica->id])->save()))
->successRedirectUrl(fn (Egg $replica) => EditEgg::getUrl(['record' => $replica])),
])
->toolbarActions([
ImportEggAction::make()
->multiple(),
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make()
->before(function (Collection &$records) {
$eggsWithServers = $records->filter(fn (Egg $egg) => $egg->servers_count > 0);
if ($eggsWithServers->isNotEmpty()) {
$eggNames = $eggsWithServers->map(fn (Egg $egg) => sprintf('%s (%d server%s)', $egg->name, $egg->servers_count, $egg->servers_count > 1 ? 's' : ''))
->join(', ');
Notification::make()
->danger()
->title(trans('admin/egg.cannot_delete', ['count' => $eggsWithServers->count()]))
->body(trans('admin/egg.eggs_have_servers', ['eggs' => $eggNames]))
->send();
}
$records = $records->filter(fn (Egg $egg) => $egg->servers_count <= 0);
if ($records->isEmpty()) {
$this->halt();
}
}),
UpdateEggBulkAction::make()
->before(function (Collection &$records) {
$eggsWithoutUpdateUrl = $records->filter(fn (Egg $egg) => $egg->update_url === null);
if ($eggsWithoutUpdateUrl->isNotEmpty()) {
$eggNames = $eggsWithoutUpdateUrl->pluck('name')->join(', ');
Notification::make()
->warning()
->title(trans('admin/egg.cannot_update', ['count' => $eggsWithoutUpdateUrl->count()]))
->body(trans('admin/egg.no_update_url', ['eggs' => $eggNames]))
->send();
}
$records = $records->filter(fn (Egg $egg) => $egg->update_url !== null);
if ($records->isEmpty()) {
$this->halt();
}
}),
]),
->groupedBulkActions([
DeleteBulkAction::make()
->before(fn (&$records) => $records = $records->filter(function ($egg) {
/** @var Egg $egg */
return $egg->servers_count <= 0;
})),
UpdateEggBulkAction::make()
->before(fn (&$records) => $records = $records->filter(function ($egg) {
/** @var Egg $egg */
return cache()->get("eggs.$egg->uuid.update", false);
})),
])
->emptyStateIcon(TablerIcon::Eggs)
->emptyStateIcon('tabler-eggs')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/egg.no_eggs'))
->emptyStateActions([
CreateAction::make(),
ImportEggAction::make()
->multiple(),
])
->filters([
TagsFilter::make()
->model(Egg::class),
]);
}
/** @return array<Action|ActionGroup>
* @throws Exception
*/
protected function getDefaultHeaderActions(): array
{
return [
ImportEggAction::make()
->multiple(),
CreateAction::make(),
];
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\Mounts;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Mounts\Pages\CreateMount;
use App\Filament\Admin\Resources\Mounts\Pages\EditMount;
use App\Filament\Admin\Resources\Mounts\Pages\ListMounts;
@@ -12,9 +11,7 @@ use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
@@ -25,6 +22,7 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Schema;
@@ -41,7 +39,7 @@ class MountResource extends Resource
protected static ?string $model = Mount::class;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::LayersLinked;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-layers-linked';
protected static ?string $recordTitleAttribute = 'name';
@@ -92,24 +90,24 @@ class MountResource extends Resource
TextColumn::make('read_only')
->label(trans('admin/mount.table.read_only'))
->badge()
->icon(fn ($state) => $state ? TablerIcon::WritingOff : TablerIcon::Writing)
->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')),
])
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
->hidden(fn ($record) => static::canEdit($record)),
EditAction::make(),
])
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
->groupedBulkActions([
DeleteBulkAction::make(),
])
->emptyStateIcon(TablerIcon::LayersLinked)
->emptyStateIcon('tabler-layers-linked')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/mount.no_mounts'));
->emptyStateHeading(trans('admin/mount.no_mounts'))
->emptyStateActions([
CreateAction::make(),
]);
}
/**
@@ -134,8 +132,8 @@ class MountResource extends Resource
true => trans('admin/mount.toggles.read_only'),
])
->icons([
false => TablerIcon::Writing,
true => TablerIcon::WritingOff,
false => 'tabler-writing',
true => 'tabler-writing-off',
])
->colors([
false => 'warning',
@@ -157,32 +155,30 @@ class MountResource extends Resource
->label(trans('admin/mount.description'))
->helperText(trans('admin/mount.description_help'))
->columnSpanFull(),
])
->columnSpan([
'default' => 1,
'lg' => 2,
])
->columns([
'default' => 1,
'xl' => 2,
])->columnSpan(1)->columns([
'default' => 1,
'lg' => 2,
]),
Group::make()->schema([
Section::make()->schema([
Select::make('eggs')->multiple()
->label(trans('admin/mount.eggs'))
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
->preload(),
Select::make('nodes')->multiple()
->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
->searchable(['name', 'fqdn'])
->preload(),
]),
Section::make()->schema([
Select::make('eggs')
->multiple()
->label(trans('admin/mount.eggs'))
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
->preload(),
Select::make('nodes')
->multiple()
->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
->searchable(['name', 'fqdn'])
->preload(),
])->columns([
'default' => 1,
'lg' => 2,
]),
])->columns([
'default' => 1,
'lg' => 3,
'lg' => 2,
]);
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
@@ -25,12 +24,7 @@ class CreateMount extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
Action::make('create')
->hiddenLabel()
->action('create')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->icon(TablerIcon::FilePlus),
$this->getCreateFormAction()->formId('form'),
];
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
@@ -23,12 +22,7 @@ class EditMount extends EditRecord
{
return [
DeleteAction::make(),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
$this->getSaveFormAction()->formId('form'),
];
}

View File

@@ -3,8 +3,12 @@
namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Models\Mount;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListMounts extends ListRecords
@@ -13,4 +17,13 @@ class ListMounts extends ListRecords
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->hidden(fn () => Mount::count() <= 0),
];
}
}

View File

@@ -3,16 +3,14 @@
namespace App\Filament\Admin\Resources\Nodes;
use App\Enums\CustomizationKey;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Nodes\Pages\CreateNode;
use App\Filament\Admin\Resources\Nodes\Pages\EditNode;
use App\Filament\Admin\Resources\Nodes\Pages\ListNodes;
use App\Filament\Admin\Resources\Nodes\RelationManagers\AllocationsRelationManager;
use App\Filament\Admin\Resources\Nodes\RelationManagers\ServersRelationManager;
use App\Filament\Admin\Resources\Nodes\RelationManagers\NodesRelationManager;
use App\Models\Node;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use BackedEnum;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
@@ -25,7 +23,7 @@ class NodeResource extends Resource
protected static ?string $model = Node::class;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Server2;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-server-2';
protected static ?string $recordTitleAttribute = 'name';
@@ -59,7 +57,7 @@ class NodeResource extends Resource
{
return [
AllocationsRelationManager::class,
ServersRelationManager::class,
NodesRelationManager::class,
];
}

View File

@@ -2,12 +2,11 @@
namespace App\Filament\Admin\Resources\Nodes\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Models\Node;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeSteps;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TagsInput;
@@ -20,7 +19,6 @@ use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Wizard;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
@@ -28,406 +26,392 @@ class CreateNode extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeSteps;
protected static string $resource = NodeResource::class;
protected static bool $canCreateAnother = false;
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Wizard::make($this->getSteps())
->columnSpanFull()
->nextAction(fn (Action $action) => $action->tooltip(fn () => $action->getLabel())->iconButton()->iconSize(IconSize::ExtraLarge)->icon(TablerIcon::ArrowRight))
->previousAction(fn (Action $action) => $action->tooltip(fn () => $action->getLabel())->iconButton()->iconSize(IconSize::ExtraLarge)->icon(TablerIcon::ArrowLeft))
Wizard::make([
Step::make('basic')
->label(trans('admin/node.tabs.basic_settings'))
->icon('tabler-server')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn'))
->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) {
if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.fqdn_help');
}
return '';
}
return trans('admin/node.error');
})
->hintColor('danger')
->hint(function ($state) {
if (is_ip($state) && request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
return '';
})
->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
}
})
->maxLength(255),
TextInput::make('ip')
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => trans('admin/node.valid'),
false => trans('admin/node.invalid'),
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'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'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer(),
TextInput::make('name')
->label(trans('admin/node.display_name'))
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection')
->label(trans('admin/node.ssl'))
->columnSpan(1)
->inline()
->helperText(function (Get $get) {
if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl'));
}
if (is_ip($get('fqdn'))) {
return trans('admin/node.ssl_help');
}
return '';
})
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([
'http' => 'HTTP',
'https' => 'HTTPS (SSL)',
'https_proxy' => 'HTTPS with (reverse) proxy',
])
->colors([
'http' => 'warning',
'https' => 'success',
'https_proxy' => 'success',
])
->icons([
'http' => 'tabler-lock-open-off',
'https' => 'tabler-lock',
'https_proxy' => 'tabler-shield-lock',
])
->default(fn () => request()->isSecure() ? 'https' : 'http')
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings'))
->icon('tabler-server-cog')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
ToggleButtons::make('maintenance_mode')
->label(trans('admin/node.maintenance_mode'))->inline()
->columnSpan(1)
->default(false)
->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help'))
->options([
true => trans('admin/node.enabled'),
false => trans('admin/node.disabled'),
])
->colors([
true => 'danger',
false => 'success',
]),
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label(trans('admin/node.use_for_deploy'))->inline()
->options([
true => trans('admin/node.yes'),
false => trans('admin/node.no'),
])
->colors([
true => 'success',
false => 'danger',
]),
TagsInput::make('tags')
->label(trans('admin/node.tags'))
->columnSpan(2),
TextInput::make('upload_size')
->label(trans('admin/node.upload_limit'))
->helperText(trans('admin/node.upload_limit_help.0'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.1'))
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label(trans('admin/node.sftp_port'))
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(2)
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_mem')
->dehydrated()
->label(trans('admin/node.memory'))->inlineLabel()->inline()
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label(trans('admin/node.memory_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label(trans('admin/node.overallocate'))->inlineLabel()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_disk')
->dehydrated()
->label(trans('admin/node.disk'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.disk_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->dehydrated()
->label(trans('admin/node.cpu'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.cpu_limit'))->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0)
->required(),
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%')
->required(),
]),
]),
])->columnSpanFull()
->nextAction(fn (Action $action) => $action->label(trans('admin/node.next_step')))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::icon-button
type="submit"
iconSize="xl"
icon="tabler-plus"
tooltip="{{ trans('admin/node.create') }}"
>
{{ trans('admin/node.create') }}
</x-filament::icon-button>
BLADE))),
<x-filament::button
type="submit"
size="sm"
>
{{ trans('admin/node.create') }}
</x-filament::button>
BLADE))),
]);
}
/** @return Step[] */
protected function getDefaultSteps(): array
{
return [
Step::make('basic')
->label(trans('admin/node.tabs.basic_settings'))
->icon(TablerIcon::Server)
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn'))
->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) {
if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.fqdn_help');
}
return '';
}
return trans('admin/node.error');
})
->hintColor('danger')
->hint(function ($state) {
if (is_ip($state) && request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
return '';
})
->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
}
})
->maxLength(255),
TextInput::make('ip')
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => trans('admin/node.valid'),
false => trans('admin/node.invalid'),
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'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'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer(),
TextInput::make('name')
->label(trans('admin/node.display_name'))
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection')
->label(trans('admin/node.ssl'))
->columnSpan(1)
->inline()
->helperText(function (Get $get) {
if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl'));
}
if (is_ip($get('fqdn'))) {
return trans('admin/node.ssl_help');
}
return '';
})
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([
'http' => 'HTTP',
'https' => 'HTTPS (SSL)',
'https_proxy' => 'HTTPS with (reverse) proxy',
])
->colors([
'http' => 'warning',
'https' => 'success',
'https_proxy' => 'success',
])
->icons([
'http' => TablerIcon::LockOpenOff,
'https' => TablerIcon::Lock,
'https_proxy' => TablerIcon::ShieldLock,
])
->default(fn () => request()->isSecure() ? 'https' : 'http')
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings'))
->icon(TablerIcon::ServerCog)
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
ToggleButtons::make('maintenance_mode')
->label(trans('admin/node.maintenance_mode'))->inline()
->columnSpan(1)
->default(false)
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.maintenance_mode_help'))
->options([
true => trans('admin/node.enabled'),
false => trans('admin/node.disabled'),
])
->colors([
true => 'danger',
false => 'success',
]),
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label(trans('admin/node.use_for_deploy'))->inline()
->options([
true => trans('admin/node.yes'),
false => trans('admin/node.no'),
])
->colors([
true => 'success',
false => 'danger',
]),
TagsInput::make('tags')
->label(trans('admin/node.tags'))
->columnSpan(2),
TextInput::make('upload_size')
->label(trans('admin/node.upload_limit'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.upload_limit_help'))
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_base')
->label(trans('admin/node.daemon_base'))
->placeholder('/var/lib/pelican/volumes')
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.daemon_base_help'))
->columnSpan(1)
->required()
->default('/var/lib/pelican/volumes')
->rule('regex:/^([\/][\d\w.\-\/]+)$/'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label(trans('admin/node.sftp_port'))
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(1)
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_mem')
->dehydrated()
->label(trans('admin/node.memory'))->inlineLabel()->inline()
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label(trans('admin/node.memory_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label(trans('admin/node.overallocate'))->inlineLabel()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_disk')
->dehydrated()
->label(trans('admin/node.disk'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.disk_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->dehydrated()
->label(trans('admin/node.cpu'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.cpu_limit'))->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0)
->required(),
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%')
->required(),
]),
]),
];
}
protected function getRedirectUrlParameters(): array
{
return [

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,14 @@
namespace App\Filament\Admin\Resources\Nodes\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Filament\Components\Tables\Columns\NodeHealthColumn;
use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Node;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ListRecords;
@@ -46,14 +47,14 @@ class ListNodes extends ListRecords
IconColumn::make('scheme')
->visibleFrom('xl')
->label('SSL')
->trueIcon(TablerIcon::Lock)
->falseIcon(TablerIcon::LockOpenOff)
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open-off')
->state(fn (Node $node) => $node->scheme === 'https'),
IconColumn::make('public')
->label(trans('admin/node.table.public'))
->visibleFrom('lg')
->trueIcon(TablerIcon::EyeCheck)
->falseIcon(TablerIcon::EyeCancel),
->trueIcon('tabler-eye-check')
->falseIcon('tabler-eye-cancel'),
TextColumn::make('servers_count')
->visibleFrom('sm')
->counts('servers')
@@ -63,15 +64,24 @@ class ListNodes extends ListRecords
->recordActions([
EditAction::make(),
])
->toolbarActions([
CreateAction::make(),
])
->emptyStateIcon(TablerIcon::Server2)
->emptyStateIcon('tabler-server-2')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/node.no_nodes'))
->emptyStateActions([
CreateAction::make(),
])
->filters([
TagsFilter::make()
->model(Node::class),
]);
}
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->hidden(fn () => Node::count() <= 0),
];
}
}

View File

@@ -2,13 +2,11 @@
namespace App\Filament\Admin\Resources\Nodes\RelationManagers;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
use App\Filament\Components\Actions\UpdateNodeAllocations;
use App\Models\Allocation;
use App\Models\Node;
use App\Services\Allocations\AssignmentService;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\DeleteBulkAction;
@@ -30,7 +28,7 @@ class AllocationsRelationManager extends RelationManager
{
protected static string $relationship = 'allocations';
protected static string|BackedEnum|null $icon = TablerIcon::PlugConnected;
protected static string|\BackedEnum|null $icon = 'tabler-plug-connected';
public function setTitle(): string
{
@@ -58,7 +56,7 @@ class AllocationsRelationManager extends RelationManager
->label(trans('admin/node.ports')),
TextColumn::make('server.name')
->label(trans('admin/node.table.servers'))
->icon(TablerIcon::BrandDocker)
->icon('tabler-brand-docker')
->visibleFrom('md')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
@@ -87,8 +85,7 @@ class AllocationsRelationManager extends RelationManager
DeleteBulkAction::make()
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
Action::make('create new allocation')
->tooltip(trans('admin/node.create_allocation'))
->icon(TablerIcon::WorldPlus)
->label(trans('admin/node.create_allocation'))
->schema(fn () => [
Select::make('allocation_ip')
->options(fn () => collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
@@ -99,8 +96,9 @@ class AllocationsRelationManager extends RelationManager
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->live()
->hintAction(
Action::make('hint_refresh')
->icon(TablerIcon::Refresh)
Action::make('refresh')
->iconButton()
->icon('tabler-refresh')
->tooltip(trans('admin/node.refresh'))
->action(function () {
cache()->forget("nodes.{$this->getOwnerRecord()->id}.ips");

View File

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

View File

@@ -1,43 +0,0 @@
<?php
namespace App\Filament\Admin\Resources\Plugins\Pages;
use App\Enums\PluginCategory;
use App\Filament\Admin\Resources\Plugins\PluginResource;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Filament\Resources\Pages\ListRecords;
use Filament\Schemas\Components\Tabs\Tab;
class ListPlugins extends ListRecords
{
protected static string $resource = PluginResource::class;
public function reorderTable(array $order, int|string|null $draggedRecordKey = null): void
{
/** @var PluginService $pluginService */
$pluginService = app(PluginService::class); // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
$pluginService->updateLoadOrder($order);
}
public function getTabs(): array
{
$tabs = [
'all' => Tab::make('all')
->label(trans('admin/plugin.all'))
->badge(Plugin::count()),
];
foreach (PluginCategory::cases() as $category) {
$query = Plugin::whereCategory($category->value);
$tabs[$category->value] = Tab::make($category->value)
->label($category->getLabel())
->icon($category->getIcon())
->badge($query->count())
->modifyQueryUsing(fn () => $query);
}
return $tabs;
}
}

View File

@@ -1,336 +0,0 @@
<?php
namespace App\Filament\Admin\Resources\Plugins;
use App\Enums\PluginStatus;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Http\UploadedFile;
class PluginResource extends Resource
{
protected static ?string $model = Plugin::class;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Packages;
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationLabel(): string
{
return trans('admin/plugin.nav_title');
}
public static function getModelLabel(): string
{
return trans('admin/plugin.model_label');
}
public static function getPluralModelLabel(): string
{
return trans('admin/plugin.model_label_plural');
}
public static function getNavigationBadge(): ?string
{
return (string) static::getEloquentQuery()->count() ?: null;
}
public static function table(Table $table): Table
{
return $table
->openRecordUrlInNewTab()
->reorderable('load_order')
->authorizeReorder(fn () => user()?->can('update plugin'))
->reorderRecordsTriggerAction(fn (Action $action, bool $isReordering) => $action->hiddenLabel()->tooltip($isReordering ? trans('admin/plugin.apply_load_order') : trans('admin/plugin.change_load_order')))
->defaultSort('load_order')
->columns([
TextColumn::make('name')
->label(trans('admin/plugin.name'))
->description(fn (Plugin $plugin) => (strlen($plugin->description) > 80) ? substr($plugin->description, 0, 80).'...' : $plugin->description)
->icon(fn (Plugin $plugin) => $plugin->isUpdateAvailable() ? TablerIcon::VersionsOff : TablerIcon::Versions)
->iconColor(fn (Plugin $plugin) => $plugin->isUpdateAvailable() ? 'danger' : 'success')
->tooltip(fn (Plugin $plugin) => $plugin->isUpdateAvailable() ? trans('admin/plugin.update_available') : null)
->sortable()
->searchable(),
TextColumn::make('author')
->label(trans('admin/plugin.author'))
->sortable(),
TextColumn::make('version')
->label(trans('admin/plugin.version'))
->sortable(),
TextColumn::make('category')
->label(trans('admin/plugin.category'))
->badge()
->sortable()
->visible(fn ($livewire) => $livewire->activeTab === 'all'),
TextColumn::make('status')
->label(trans('admin/plugin.status'))
->badge()
->tooltip(fn (Plugin $plugin) => $plugin->status_message)
->sortable(),
])
->recordActions([
Action::make('exclude_view')
->label(trans('filament-actions::view.single.label'))
->icon(fn (Plugin $plugin) => $plugin->getReadme() ? TablerIcon::Eye : TablerIcon::EyeShare)
->color('gray')
->visible(fn (Plugin $plugin) => $plugin->getReadme() || $plugin->url)
->url(fn (Plugin $plugin) => !$plugin->getReadme() ? $plugin->url : null, true)
->slideOver(true)
->modalHeading('Readme')
->modalSubmitAction(fn (Plugin $plugin) => Action::make('exclude_visit_website')
->label(trans('admin/plugin.visit_website'))
->visible(!is_null($plugin->url))
->url($plugin->url, true)
)
->modalCancelActionLabel(trans('filament::components/modal.actions.close.label'))
->schema(fn (Plugin $plugin) => $plugin->getReadme() ? [
TextEntry::make('readme')
->hiddenLabel()
->markdown()
->state(fn (Plugin $plugin) => $plugin->getReadme()),
] : null),
Action::make('exclude_settings')
->label(trans('admin/plugin.settings'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon(TablerIcon::Settings)
->color('primary')
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::Enabled && $plugin->hasSettings())
->schema(fn (Plugin $plugin) => $plugin->getSettingsForm())
->action(fn (array $data, Plugin $plugin) => $plugin->saveSettings($data))
->slideOver(),
ActionGroup::make([
Action::make('exclude_install')
->label(trans('admin/plugin.install'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon(TablerIcon::Terminal)
->color('success')
->hidden(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
try {
$pluginService->installPlugin($plugin, !$plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.installed'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.install_error'))
->body($exception->getMessage())
->send();
}
}),
Action::make('exclude_update')
->label(trans('admin/plugin.update'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon(TablerIcon::Download)
->color('success')
->visible(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled && $plugin->isUpdateAvailable())
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
try {
$pluginService->updatePlugin($plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.updated'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.update_error'))
->body($exception->getMessage())
->send();
}
}),
Action::make('exclude_enable')
->label(trans('admin/plugin.enable'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon(TablerIcon::Check)
->color('success')
->visible(fn (Plugin $plugin) => $plugin->canEnable())
->requiresConfirmation(fn (Plugin $plugin, PluginService $pluginService) => $plugin->isTheme() && $pluginService->hasThemePluginEnabled())
->modalHeading(fn (Plugin $plugin, PluginService $pluginService) => $plugin->isTheme() && $pluginService->hasThemePluginEnabled() ? trans('admin/plugin.enable_theme_modal.heading') : null)
->modalDescription(fn (Plugin $plugin, PluginService $pluginService) => $plugin->isTheme() && $pluginService->hasThemePluginEnabled() ? trans('admin/plugin.enable_theme_modal.description') : null)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->enablePlugin($plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.enabled'))
->send();
}),
Action::make('exclude_disable')
->label(trans('admin/plugin.disable'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon(TablerIcon::X)
->color('warning')
->visible(fn (Plugin $plugin) => $plugin->canDisable())
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->disablePlugin($plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.disabled'))
->send();
}),
Action::make('exclude_delete')
->label(trans('filament-actions::delete.single.label'))
->authorize(fn (Plugin $plugin) => user()?->can('delete', $plugin))
->icon(TablerIcon::Trash)
->color('danger')
->requiresConfirmation()
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->deletePlugin($plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.deleted'))
->send();
}),
Action::make('exclude_uninstall')
->label(trans('admin/plugin.uninstall'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon(TablerIcon::Terminal)
->color('danger')
->requiresConfirmation()
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
try {
$pluginService->uninstallPlugin($plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.uninstalled'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.uninstall_error'))
->body($exception->getMessage())
->send();
}
}),
]),
])
->headerActions([
Action::make('import_from_file')
->hiddenLabel()
->tooltip(trans('admin/plugin.import_from_file'))
->authorize(fn () => user()?->can('create', Plugin::class))
->icon(TablerIcon::FileDownload)
->schema([
// TODO: switch to new file upload
FileUpload::make('file')
->required()
->acceptedFileTypes(['application/zip', 'application/zip-compressed', 'application/x-zip-compressed'])
->preserveFilenames()
->previewable(false)
->storeFiles(false),
])
->action(function ($data, $livewire, PluginService $pluginService) {
try {
/** @var UploadedFile $file */
$file = $data['file'];
$pluginName = str($file->getClientOriginalName())->before('.zip')->toString();
if (Plugin::where('id', $pluginName)->exists()) {
throw new Exception(trans('admin/plugin.notifications.import_exists'));
}
$pluginService->downloadPluginFromFile($file);
Notification::make()
->success()
->title(trans('admin/plugin.notifications.imported'))
->send();
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
} catch (Exception $exception) {
report($exception);
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.import_failed'))
->body($exception->getMessage())
->send();
}
}),
Action::make('import_from_url')
->hiddenLabel()
->tooltip(trans('admin/plugin.import_from_url'))
->authorize(fn () => user()?->can('create', Plugin::class))
->icon(TablerIcon::WorldDownload)
->schema([
TextInput::make('url')
->required()
->url()
->endsWith('.zip'),
])
->action(function ($data, $livewire, PluginService $pluginService) {
try {
$pluginName = str($data['url'])->before('.zip')->explode('/')->last();
if (Plugin::where('id', $pluginName)->exists()) {
throw new Exception(trans('admin/plugin.notifications.import_exists'));
}
$pluginService->downloadPluginFromUrl($data['url']);
Notification::make()
->success()
->title(trans('admin/plugin.notifications.imported'))
->send();
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
} catch (Exception $exception) {
report($exception);
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.import_failed'))
->body($exception->getMessage())
->send();
}
}),
])
->emptyStateIcon(TablerIcon::Packages)
->emptyStateDescription('')
->emptyStateHeading(trans('admin/plugin.no_plugins'));
}
public static function getPages(): array
{
return [
'index' => ListPlugins::route('/'),
];
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\Roles\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Roles\RoleResource;
use App\Models\Role;
use App\Traits\Filament\CanCustomizeHeaderActions;
@@ -32,12 +31,7 @@ class CreateRole extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
Action::make('create')
->hiddenLabel()
->action('create')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->icon(TablerIcon::FilePlus),
$this->getCreateFormAction()->formId('form'),
];
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Admin\Resources\Roles\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Roles\RoleResource;
use App\Models\Role;
use App\Traits\Filament\CanCustomizeHeaderActions;
@@ -58,14 +57,8 @@ class EditRole extends EditRecord
{
return [
DeleteAction::make()
->tooltip(fn (Role $role) => $role->isRootAdmin() ? trans('admin/role.root_admin_delete') : ($role->users_count >= 1 ? trans('admin/role.in_use') : trans('filament-actions::delete.single.label')))
->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1)
->label(fn (Role $role) => $role->isRootAdmin() ? trans('admin/role.root_admin_delete') : ($role->users_count >= 1 ? trans('admin/role.in_use') : trans('filament-actions::delete.single.label'))), $this->getSaveFormAction()->formId('form'),
];
}

View File

@@ -5,6 +5,9 @@ namespace App\Filament\Admin\Resources\Roles\Pages;
use App\Filament\Admin\Resources\Roles\RoleResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListRoles extends ListRecords
@@ -13,4 +16,12 @@ class ListRoles extends ListRecords
use CanCustomizeHeaderWidgets;
protected static string $resource = RoleResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Filament\Admin\Resources\Roles;
use App\Enums\CustomizationKey;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Roles\Pages\CreateRole;
use App\Filament\Admin\Resources\Roles\Pages\EditRole;
use App\Filament\Admin\Resources\Roles\Pages\ListRoles;
@@ -16,7 +15,6 @@ use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
@@ -46,7 +44,7 @@ class RoleResource extends Resource
protected static ?string $model = Role::class;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::UsersGroup;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-users-group';
protected static ?string $recordTitleAttribute = 'name';
@@ -100,16 +98,19 @@ class RoleResource extends Resource
])
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
->hidden(fn ($record) => static::canEdit($record)),
EditAction::make(),
])
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0)
->groupedBulkActions([
DeleteBulkAction::make(),
])
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0);
->emptyStateIcon('tabler-users-group')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/role.no_roles'))
->emptyStateActions([
CreateAction::make(),
]);
}
/**
@@ -123,7 +124,7 @@ class RoleResource extends Resource
$options = [];
foreach ($permissions as $permission) {
$options[$permission . ' ' . $model] = Str::headline($permission);
$options[$permission . ' ' . strtolower($model)] = Str::headline($permission);
}
$permissionSections[] = self::makeSection($model, $options);
@@ -166,11 +167,23 @@ class RoleResource extends Resource
*/
private static function makeSection(string $model, array $options): Section
{
$model = ucwords($model);
$icon = null;
if (class_exists('\App\Filament\Admin\Resources\\' . $model . 'Resource')) {
$icon = ('\App\Filament\Admin\Resources\\' . $model . 'Resource')::getNavigationIcon();
} elseif (class_exists('\App\Filament\Admin\Pages\\' . $model)) {
$icon = ('\App\Filament\Admin\Pages\\' . $model)::getNavigationIcon();
} elseif (class_exists('\App\Filament\Server\Resources\\' . $model . 'Resource')) {
$icon = ('\App\Filament\Server\Resources\\' . $model . 'Resource')::getNavigationIcon();
}
return Section::make(Str::headline($model))
->columnSpan(1)
->collapsible()
->collapsed()
->icon(Role::getModelIcon($model))
->icon($icon)
->headerActions([
Action::make('count')
->label(fn (Get $get) => count($get(strtolower($model) . '_list')))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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