Compare commits

..

3 Commits

Author SHA1 Message Date
RMartinOscar
1d6f490c77 5min timeout 2025-09-30 16:41:00 +00:00
MartinOscar
e2851f28ca Rename shift to shift.yaml 2025-09-30 15:35:57 +02:00
MartinOscar
de14e54931 Create shift workflow 2025-09-30 15:31:47 +02:00
1097 changed files with 13209 additions and 43647 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

20
.github/workflows/shift.yaml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Shift
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 5"
jobs:
shift:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Shift
run: |
curl -X POST -s -retry 5 -m 60 --fail-with-body \
https://laravelshift.com/api/run \
-H "Accept: application/json" \
-d "api_token=${{ secrets.SHIFT_TOKEN }}" \
-d "code=${{ secrets.SHIFT_CODE }}" \
-d "scs=github:${{ github.repository }}:${{ github.ref_name }}"

1
.gitignore vendored
View File

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

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

@@ -35,7 +35,7 @@ class DeleteUserCommand extends Command
if ($this->input->isInteractive()) {
$tableValues = [];
foreach ($results as $user) {
$tableValues[] = [$user->id, $user->email, $user->username];
$tableValues[] = [$user->id, $user->email, $user->name];
}
$this->table(['User ID', 'Email', 'Name'], $tableValues);

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) {
@@ -20,9 +18,8 @@ enum CustomizationKey: string
self::ConsoleFont => 'monospace',
self::ConsoleFontSize => 14,
self::ConsoleGraphPeriod => 30,
self::TopNavigation => config('panel.filament.default-navigation', 'sidebar'),
self::TopNavigation => false,
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 () => !auth()->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 () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->schema([
TextEntry::make('java')
->label('Please select a supported version from the list below to continue starting the server.'),
@@ -56,7 +56,8 @@ class JavaVersionSchema implements FeatureSchemaInterface
->default(fn () => $server->image)
->notIn(fn () => $server->image)
->required()
->preload(),
->preload()
->native(false),
])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) {
try {

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,10 +31,10 @@ class PIDLimitSchema implements FeatureSchemaInterface
{
return Action::make($this->getId())
->requiresConfirmation()
->icon(TablerIcon::AlertTriangle)
->modalHeading(fn () => user()?->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->icon('tabler-alert-triangle')
->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML'
auth()->user()->isAdmin() ? <<<'HTML'
<p>
This server has reached the maximum process or memory limit.
</p>

View File

@@ -29,7 +29,7 @@ class SteamDiskSpaceSchema implements FeatureSchemaInterface
->requiresConfirmation()
->modalHeading('Out of available disk space...')
->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML'
auth()->user()->isAdmin() ? <<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process.

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,9 +2,7 @@
namespace App\Extensions\OAuth;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Laravel\Socialite\Contracts\User as OAuthUser;
use SocialiteProviders\Manager\SocialiteWasCalled;
class OAuthService
@@ -45,27 +43,4 @@ class OAuthService
$this->schemas[$schema->getId()] = $schema;
}
public function linkUser(User $user, OAuthSchemaInterface $schema, OAuthUser $oauthUser): User
{
$oauth = $user->oauth ?? [];
$oauth[$schema->getId()] = $oauthUser->getId();
$user->update(['oauth' => $oauth]);
return $user->refresh();
}
public function unlinkUser(User $user, OAuthSchemaInterface $schema): User
{
$oauth = $user->oauth ?? [];
if (!isset($oauth[$schema->getId()])) {
return $user;
}
unset($oauth[$schema->getId()]);
$user->update(['oauth' => $oauth]);
return $user->refresh();
}
}

View File

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

View File

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

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

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

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

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

View File

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

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

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

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

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

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';
@@ -41,16 +39,15 @@ class Health extends Page
public static function canAccess(): bool
{
return user()?->can('view health');
return auth()->user()->can('view health');
}
protected function getActions(): array
{
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

@@ -1,128 +0,0 @@
<?php
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\Tables\Table;
use Illuminate\Support\Facades\Http;
class ListLogs extends BaseListLogs
{
protected string $view = 'filament.components.list-logs';
public function getHeading(): string|null|\Illuminate\Contracts\Support\Htmlable
{
return trans('admin/log.navigation.panel_logs');
}
public static function table(Table $table): Table
{
return parent::table($table)
->emptyStateHeading(trans('admin/log.empty_table'))
->emptyStateIcon(TablerIcon::Check)
->columns([
NameColumn::make('date'),
LevelColumn::make(Level::ALL)
->tooltip(trans('admin/log.total_logs')),
LevelColumn::make(Level::Error)
->tooltip(trans('admin/log.error')),
LevelColumn::make(Level::Warning)
->tooltip(trans('admin/log.warning')),
LevelColumn::make(Level::Notice)
->tooltip(trans('admin/log.notice')),
LevelColumn::make(Level::Info)
->tooltip(trans('admin/log.info')),
LevelColumn::make(Level::Debug)
->tooltip(trans('admin/log.debug')),
])
->recordActions([
ViewLogAction::make()
->icon(TablerIcon::FileDescription)->iconButton(),
DownloadAction::make()
->tooltip(fn ($record) => trans('filament-log-viewer::log.table.actions.download.label', ['log' => ParseDateUseCase::execute($record['date'])]))
->icon(TablerIcon::FileDownload)->iconButton(),
Action::make('uploadLogs')
->hiddenLabel()
->tooltip(trans('admin/log.actions.upload_tooltip', ['url' => 'logs.pelican.dev']))
->icon(TablerIcon::WorldUpload)
->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);
if (!file_exists($logPath)) {
Notification::make()
->title(trans('admin/log.actions.log_not_found'))
->body(trans('admin/log.actions.log_not_found_description', ['filename' => $record['date']]))
->danger()
->send();
return;
}
$lines = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$totalLines = count($lines);
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
try {
$response = Http::timeout(10)
->asMultipart()
->attach('c', $content)
->attach('e', '14d')
->post('https://logs.pelican.dev');
if ($response->failed()) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body(trans('admin/log.actions.failed_to_upload_description', ['status' => $response->status()]))
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/log.actions.log_upload'))
->body("{$url}")
->success()
->actions([
Action::make('exclude_viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body($e->getMessage())
->danger()
->send();
return;
}
}),
DeleteAction::make()
->icon(TablerIcon::Trash)->iconButton(),
]);
}
}

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';
@@ -85,7 +82,7 @@ class Settings extends Page implements HasSchemas
public static function canAccess(): bool
{
return user()?->can('view settings');
return auth()->user()->can('view settings');
}
public function getTitle(): string
@@ -98,52 +95,46 @@ class Settings extends Page implements HasSchemas
return trans('admin/setting.title');
}
/** @return array<Component> */
/**
* @return array<Component>
*
* @throws Exception
*/
protected function getFormSchema(): array
{
return [
Tabs::make('Tabs')
->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()),
->disabled(fn () => !auth()->user()->can('update settings'))
->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))
@@ -190,14 +181,15 @@ class Settings extends Page implements HasSchemas
->schema([
Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider'))
->native(false)
->options($this->avatarService->getMappings())
->selectablePlaceholder(false)
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
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))
@@ -212,15 +204,6 @@ class Settings extends Page implements HasSchemas
])
->stateCast(new BooleanStateCast(false, true))
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
ToggleButtons::make('FILAMENT_DEFAULT_NAVIGATION')
->label(trans('admin/setting.general.default_navigation'))
->inline()
->options([
'sidebar' => trans('admin/setting.general.sidebar'),
'topbar' => trans('admin/setting.general.topbar'),
'mixed' => trans('admin/setting.general.mixed'),
])
->default(env('FILAMENT_DEFAULT_NAVIGATION', config('panel.filament.default-navigation'))),
ToggleButtons::make('APP_2FA_REQUIRED')
->label(trans('admin/setting.general.2fa_requirement'))
->inline()
@@ -234,6 +217,7 @@ class Settings extends Page implements HasSchemas
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
Select::make('FILAMENT_WIDTH')
->label(trans('admin/setting.general.display_width'))
->native(false)
->options(Width::class)
->selectablePlaceholder(false)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
@@ -244,17 +228,17 @@ 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'))
->authorize(fn () => auth()->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)
->authorize(fn () => user()?->can('update settings'))
->icon('tabler-brand-cloudflare')
->authorize(fn () => auth()->user()->can('update settings'))
->action(function (Factory $client, Set $set) {
$ips = collect();
@@ -264,7 +248,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 +279,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 +289,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,11 +331,11 @@ 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'))
->authorize(fn () => auth()->user()->can('update settings'))
->action(function (Get $get) {
// Store original mail configuration
$originalConfig = [
@@ -386,8 +368,8 @@ class Settings extends Page implements HasSchemas
'services.mailgun.endpoint' => $get('MAILGUN_ENDPOINT'),
]);
MailNotification::route('mail', user()?->email)
->notify(new MailTested(user()));
MailNotification::route('mail', auth()->user()->email)
->notify(new MailTested(auth()->user()));
Notification::make()
->title(trans('admin/setting.mail.test_mail_sent'))
@@ -541,8 +523,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 +545,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 +607,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 +640,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 +650,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 +699,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 +736,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 +749,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 +798,7 @@ class Settings extends Page implements HasSchemas
$this->writeToEnvironment($data);
Artisan::call('config:clear');
Artisan::call('queue:restart');
$this->redirect($this->getUrl());
@@ -852,11 +821,8 @@ 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'))
->authorize(fn () => auth()->user()->can('update settings'))
->keyBindings(['mod+s']),
];

View File

@@ -1,101 +0,0 @@
<?php
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use App\Traits\ResolvesRecordDate;
use Boquizo\FilamentLogViewer\Actions\BackAction;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Pages\ViewLog as BaseViewLog;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Http;
class ViewLogs extends BaseViewLog
{
use ResolvesRecordDate;
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(),
DownloadAction::make(withTooltip: true)
->icon(TablerIcon::FileDownload)->iconButton(),
Action::make('uploadLogs')
->hiddenLabel()
->icon(TablerIcon::WorldUpload)
->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);
if (!file_exists($logPath)) {
Notification::make()
->title(trans('admin/log.actions.log_not_found'))
->body(trans('admin/log.actions.log_not_found_description', ['filename' => $this->resolveRecordDate()]))
->danger()
->send();
return;
}
$lines = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$totalLines = count($lines);
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
try {
$response = Http::timeout(10)
->asMultipart()
->attach('c', $content)
->attach('e', '14d')
->post('https://logs.pelican.dev');
if ($response->failed()) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body(trans('admin/log.actions.failed_to_upload_description', ['status' => $response->status()]))
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/log.actions.log_upload'))
->body("{$url}")
->success()
->actions([
Action::make('exclude_viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body($e->getMessage())
->danger()
->send();
return;
}
}),
];
}
}

View File

@@ -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')
@@ -96,17 +93,17 @@ class ApiKeyResource extends Resource
->sortable(),
TextColumn::make('user.username')
->label(trans('admin/apikey.table.created_by'))
->url(fn (ApiKey $apiKey) => user()?->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
])
->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'),
];
}
@@ -44,7 +38,7 @@ class CreateApiKey extends CreateRecord
{
$data['identifier'] = ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION);
$data['token'] = Str::random(ApiKey::KEY_LENGTH);
$data['user_id'] = user()?->id;
$data['user_id'] = auth()->user()->id;
$data['key_type'] = ApiKey::TYPE_APPLICATION;
$permissions = [];

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(),
]);
}
/**
@@ -169,7 +166,7 @@ class DatabaseHostResource extends Resource
->preload()
->helperText(trans('admin/databasehost.linked_nodes_help'))
->label(trans('admin/databasehost.linked_nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'))),
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
]),
]);
}
@@ -199,7 +196,7 @@ class DatabaseHostResource extends Resource
return $query->where(function (Builder $query) {
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'));
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
});
}

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;
@@ -158,7 +157,7 @@ class CreateDatabaseHost extends CreateRecord
->preload()
->helperText(trans('admin/databasehost.linked_nodes_help'))
->label(trans('admin/databasehost.linked_nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'))),
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
]),
];
}
@@ -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';
@@ -34,7 +32,7 @@ class EggResource extends Resource
public static function getNavigationGroup(): ?string
{
return user()?->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
return auth()->user()->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
}
public static function getNavigationLabel(): string

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,225 @@ 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')),
Textarea::make('startup')
->label(trans('admin/egg.startup'))
->rows(3)
->columnSpanFull()
->required()
->placeholder(implode("\n", [
'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()
->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_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'))
->native(false)
->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,24 +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\FileUpload;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
@@ -28,420 +24,241 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Flex;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\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)
->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);
return;
}
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));
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new Exception(trans('admin/egg.import.no_local_ip'));
}
$set('imageUrl', $state);
$set('imageExtension', $extension);
$set('image_url_error', null);
} 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(),
]),
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);
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
if (!empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
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);
}
}
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;
})
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->icon('tabler-egg')
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->helperText(trans('admin/egg.name_help')),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->disabled(),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.description_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Textarea::make('startup')
->label(trans('admin/egg.startup'))
->rows(3)
->columnSpanFull()
->required()
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->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_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)
->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'))
->columnSpanFull()
->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'))
->native(false)
->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 +267,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 +280,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,15 @@ 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
@@ -45,13 +42,6 @@ class ListEggs extends ListRecords
TextColumn::make('id')
->label('Id')
->hidden(),
ImageColumn::make('image')
->label('')
->alignCenter()
->circular()
->getStateUsing(fn ($record) => $record->image
? $record->image
: 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')))),
TextColumn::make('name')
->label(trans('admin/egg.name'))
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
@@ -64,76 +54,62 @@ 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'])
->beforeReplicaSaved(function (Egg $replica) {
$replica->author = user()?->email;
$replica->author = auth()->user()->email;
$replica->name .= ' Copy';
$replica->uuid = Str::uuid()->toString();
})
->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,8 +22,8 @@ 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;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -41,7 +38,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 +89,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(),
]);
}
/**
@@ -128,21 +125,21 @@ class MountResource extends Resource
ToggleButtons::make('read_only')
->label(trans('admin/mount.read_only'))
->helperText(trans('admin/mount.read_only_help'))
->stateCast(new BooleanStateCast(false, true))
->options([
false => trans('admin/mount.toggles.writable'),
true => trans('admin/mount.toggles.read_only'),
])
->icons([
false => TablerIcon::Writing,
true => TablerIcon::WritingOff,
false => 'tabler-writing',
true => 'tabler-writing-off',
])
->colors([
false => 'warning',
true => 'success',
])
->inline()
->default(false),
->default(false)
->required(),
TextInput::make('source')
->label(trans('admin/mount.source'))
->required()
@@ -157,32 +154,29 @@ 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'))
->relationship('eggs', 'name')
->preload(),
Select::make('nodes')->multiple()
->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')))
->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,
]);
}
@@ -203,7 +197,7 @@ class MountResource extends Resource
return $query->where(function (Builder $query) {
return $query->whereHas('nodes', function (Builder $query) {
$query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'));
$query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
})->orDoesntHave('nodes');
});
}

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';
@@ -46,7 +44,7 @@ class NodeResource extends Resource
public static function getNavigationGroup(): ?string
{
return user()?->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
return auth()->user()->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
}
public static function getNavigationBadge(): ?string
@@ -59,7 +57,7 @@ class NodeResource extends Resource
{
return [
AllocationsRelationManager::class,
ServersRelationManager::class,
NodesRelationManager::class,
];
}
@@ -77,6 +75,6 @@ class NodeResource extends Resource
{
$query = parent::getEloquentQuery();
return $query->whereIn('id', user()?->accessibleNodes()->pluck('id'));
return $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
}
}

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,10 @@
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 +27,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 +55,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]) : ''),
@@ -83,12 +80,9 @@ class AllocationsRelationManager extends RelationManager
->searchable()
->label(trans('admin/node.table.ip')),
])
->toolbarActions([
DeleteBulkAction::make()
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
->headerActions([
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 +93,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");
@@ -123,9 +118,10 @@ class AllocationsRelationManager extends RelationManager
->required(),
])
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
UpdateNodeAllocations::make()
->nodeRecord($this->getOwnerRecord())
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
])
->groupedBulkActions([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('update', $this->getOwnerRecord())),
]);
}
}

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

@@ -32,7 +32,7 @@ class NodeCpuChart extends ChartWidget
$this->cpuHistory = session("{$sessionKey}.cpu_history", []);
$this->cpuHistory[] = [
'cpu' => round($data['cpu_percent'] * $this->threads, 2),
'timestamp' => now(user()->timezone ?? 'UTC')->format('H:i:s'),
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
];
$this->cpuHistory = array_slice($this->cpuHistory, -60);
@@ -50,7 +50,7 @@ class NodeCpuChart extends ChartWidget
],
],
'labels' => array_column($this->cpuHistory, 'timestamp'),
'locale' => user()->language ?? 'en',
'locale' => auth()->user()->language ?? 'en',
];
}

View File

@@ -34,7 +34,7 @@ class NodeMemoryChart extends ChartWidget
'memory' => round(config('panel.use_binary_prefix')
? $data['memory_used'] / 1024 / 1024 / 1024
: $data['memory_used'] / 1000 / 1000 / 1000, 2),
'timestamp' => now(user()->timezone ?? 'UTC')->format('H:i:s'),
'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
];
$this->memoryHistory = array_slice($this->memoryHistory, -60);
@@ -52,7 +52,7 @@ class NodeMemoryChart extends ChartWidget
],
],
'labels' => array_column($this->memoryHistory, 'timestamp'),
'locale' => user()->language ?? 'en',
'locale' => auth()->user()->language ?? 'en',
];
}

View File

@@ -60,7 +60,7 @@ class NodeStorageChart extends ChartWidget
],
],
'labels' => [trans('admin/node.used'), trans('admin/node.unused')],
'locale' => user()->language ?? 'en',
'locale' => auth()->user()->language ?? 'en',
];
}

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