mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
111 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4668a517 | ||
|
|
6125b07afa | ||
|
|
9717aa4b5f | ||
|
|
9491322d8c | ||
|
|
8ed6bb4d8b | ||
|
|
a787af7a06 | ||
|
|
d9016702d6 | ||
|
|
d565441b6a | ||
|
|
cb522b24ef | ||
|
|
b85b17f080 | ||
|
|
47bd7289b1 | ||
|
|
a9b76a0f51 | ||
|
|
8eebb82eba | ||
|
|
b3501be6ec | ||
|
|
ac67656d82 | ||
|
|
968239beb3 | ||
|
|
7514206186 | ||
|
|
1a8321c937 | ||
|
|
340ae8099b | ||
|
|
9d02aeb130 | ||
|
|
cf57c28c40 | ||
|
|
382dcb3868 | ||
|
|
f793b49a81 | ||
|
|
41ddae1ba0 | ||
|
|
e717e20996 | ||
|
|
b5145b016b | ||
|
|
95a8f72058 | ||
|
|
19548338ee | ||
|
|
a8356fc5d2 | ||
|
|
7a447b04d5 | ||
|
|
45699e1614 | ||
|
|
cde3546889 | ||
|
|
3f9c1dbc3c | ||
|
|
bc2df22d78 | ||
|
|
1a3dc5c743 | ||
|
|
fdd1b3798c | ||
|
|
288cbee32f | ||
|
|
a70a060350 | ||
|
|
590569a131 | ||
|
|
7acc8782bb | ||
|
|
f3de185508 | ||
|
|
291b514e24 | ||
|
|
86c369d7ce | ||
|
|
5f77deb1fd | ||
|
|
5f4429e2c3 | ||
|
|
1df3e8d5b0 | ||
|
|
ecb195b2c4 | ||
|
|
86e8a6371e | ||
|
|
d653edb22e | ||
|
|
741252e395 | ||
|
|
308601e6fe | ||
|
|
3933222d98 | ||
|
|
c53ef78d89 | ||
|
|
60792c05c2 | ||
|
|
94420d06be | ||
|
|
6655ccca6e | ||
|
|
a193b4f5ab | ||
|
|
3d5c8d14bd | ||
|
|
de002324d7 | ||
|
|
bcbacb47cd | ||
|
|
e9f6fbadd4 | ||
|
|
c621d2dad5 | ||
|
|
64943aa50c | ||
|
|
020e41cbbc | ||
|
|
e162374e15 | ||
|
|
81c75f7966 | ||
|
|
2be8168468 | ||
|
|
465a372000 | ||
|
|
f0c536c045 | ||
|
|
6a8e630444 | ||
|
|
71aed151d9 | ||
|
|
bb5955cff4 | ||
|
|
38be89a71e | ||
|
|
deb6603840 | ||
|
|
c7a307af6e | ||
|
|
8740f0f645 | ||
|
|
466f9f7edc | ||
|
|
d21740d458 | ||
|
|
1bf6a880fb | ||
|
|
96acd268be | ||
|
|
c0a41acf1f | ||
|
|
75e89b2d4c | ||
|
|
54ea55d426 | ||
|
|
207d875df8 | ||
|
|
ff0215afed | ||
|
|
f357c9501f | ||
|
|
71116e81ba | ||
|
|
f2063d7506 | ||
|
|
c5c05150d8 | ||
|
|
214eb5874f | ||
|
|
b14f6e1645 | ||
|
|
04b251d125 | ||
|
|
5f9ee09ebd | ||
|
|
2fb85f8236 | ||
|
|
4eba5b3f7a | ||
|
|
f95ba6447c | ||
|
|
c0eedc16e0 | ||
|
|
3c5da1cd70 | ||
|
|
8638e53f2b | ||
|
|
3ec90264bd | ||
|
|
e23a4a667a | ||
|
|
a946669dc8 | ||
|
|
6a8ff1a186 | ||
|
|
b003404aea | ||
|
|
45b73debc2 | ||
|
|
329a3993c1 | ||
|
|
da7cba3203 | ||
|
|
6c205a744d | ||
|
|
e78f7bc054 | ||
|
|
12a189f585 | ||
|
|
af4cba341a |
29
.env.example
29
.env.example
@@ -1,32 +1,7 @@
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://panel.test
|
||||
APP_INSTALLED=false
|
||||
APP_TIMEZONE=UTC
|
||||
APP_LOCALE=en
|
||||
|
||||
LOG_CHANNEL=daily
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=file
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_HOST=smtp.example.com
|
||||
MAIL_PORT=25
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS=no-reply@example.com
|
||||
MAIL_FROM_NAME="Pelican Admin"
|
||||
# Set this to your domain to prevent it defaulting to 'localhost', causing mail servers such as Gmail to reject your mail
|
||||
# MAIL_EHLO_DOMAIN=panel.example.com
|
||||
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
15
.github/docker/entrypoint.sh
vendored
15
.github/docker/entrypoint.sh
vendored
@@ -21,10 +21,14 @@ else
|
||||
echo -e "APP_KEY exists in environment, using that."
|
||||
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
|
||||
fi
|
||||
|
||||
## enable installer
|
||||
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
|
||||
fi
|
||||
|
||||
mkdir /pelican-data/database
|
||||
ln -s /pelican-data/.env /var/www/html/
|
||||
chown -h www-data:www-data /var/www/html/.env
|
||||
ln -s /pelican-data/database/database.sqlite /var/www/html/database/
|
||||
|
||||
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
|
||||
@@ -38,6 +42,9 @@ fi
|
||||
echo -e "Migrating Database"
|
||||
php artisan migrate --force
|
||||
|
||||
echo -e "Optimizing Filament"
|
||||
php artisan filament:optimize
|
||||
|
||||
## start cronjobs for the queue
|
||||
echo -e "Starting cron jobs."
|
||||
crond -L /var/log/crond -l 5
|
||||
@@ -45,14 +52,14 @@ crond -L /var/log/crond -l 5
|
||||
export SUPERVISORD_CADDY=false
|
||||
|
||||
## disable caddy if SKIP_CADDY is set
|
||||
if [[ -z $SKIP_CADDY ]]; then
|
||||
if [[ "${SKIP_CADDY:-}" == "true" ]]; then
|
||||
echo "Starting PHP-FPM only"
|
||||
else
|
||||
echo "Starting PHP-FPM and Caddy"
|
||||
export SUPERVISORD_CADDY=true
|
||||
else
|
||||
echo "Starting PHP-FPM only"
|
||||
fi
|
||||
|
||||
chown -R www-data:www-data . /pelican-data/.env /pelican-data/database
|
||||
chown -R www-data:www-data /pelican-data/.env /pelican-data/database
|
||||
|
||||
echo "Starting Supervisord"
|
||||
exec "$@"
|
||||
|
||||
8
.github/docker/supervisord.conf
vendored
8
.github/docker/supervisord.conf
vendored
@@ -1,5 +1,7 @@
|
||||
[unix_http_server]
|
||||
file=/tmp/supervisor.sock ; path to your socket file
|
||||
username=dummy
|
||||
password=dummy
|
||||
|
||||
[supervisord]
|
||||
logfile=/var/log/supervisord/supervisord.log ; supervisord log file
|
||||
@@ -18,6 +20,8 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
|
||||
username=dummy
|
||||
password=dummy
|
||||
|
||||
[program:php-fpm]
|
||||
command=/usr/local/sbin/php-fpm -F
|
||||
@@ -25,7 +29,7 @@ autostart=true
|
||||
autorestart=true
|
||||
|
||||
[program:queue-worker]
|
||||
command=/usr/local/bin/php /var/www/html/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
|
||||
command=/usr/local/bin/php /var/www/html/artisan queue:work --tries=3
|
||||
user=www-data
|
||||
autostart=true
|
||||
autorestart=true
|
||||
@@ -36,4 +40,4 @@ autostart=%(ENV_SUPERVISORD_CADDY)s
|
||||
autorestart=%(ENV_SUPERVISORD_CADDY)s
|
||||
priority=10
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
|
||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -3,10 +3,8 @@ name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
mysql:
|
||||
@@ -89,7 +87,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
|
||||
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
|
||||
86
.github/workflows/docker-publish.yml
vendored
Normal file
86
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Docker
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
|
||||
if: "!contains(github.ref, 'main') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get Build Information
|
||||
id: build_info
|
||||
run: |
|
||||
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push (tag)
|
||||
uses: docker/build-push-action@v5
|
||||
if: "github.event_name == 'release' && github.event.action == 'published'"
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.build_info.outputs.version_tag }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
|
||||
- name: Build and Push (main)
|
||||
uses: docker/build-push-action@v5
|
||||
if: "github.event_name == 'push' && contains(github.ref, 'main')"
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=dev-${{ steps.build_info.outputs.short_sha }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
11
Dockerfile
11
Dockerfile
@@ -7,7 +7,9 @@ WORKDIR /build
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN yarn install --frozen-lockfile && yarn run build:production
|
||||
RUN yarn config set network-timeout 300000 \
|
||||
&& yarn install --frozen-lockfile \
|
||||
&& yarn run build:production
|
||||
|
||||
FROM php:8.3-fpm-alpine
|
||||
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
@@ -36,8 +38,8 @@ RUN touch .env
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Set file permissions
|
||||
RUN chmod -R 755 /var/www/html/storage \
|
||||
&& chmod -R 755 /var/www/html/bootstrap/cache
|
||||
RUN chmod -R 755 storage bootstrap/cache \
|
||||
&& chown -R www-data:www-data ./
|
||||
|
||||
# Add scheduler to cron
|
||||
RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -
|
||||
@@ -49,8 +51,7 @@ RUN cp .github/docker/supervisord.conf /etc/supervisord.conf && \
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
|
||||
EXPOSE 80:2019
|
||||
EXPOSE 443
|
||||
EXPOSE 80 443
|
||||
|
||||
VOLUME /pelican-data
|
||||
|
||||
|
||||
43
app/Console/Commands/Egg/CheckEggUpdatesCommand.php
Normal file
43
app/Console/Commands/Egg/CheckEggUpdatesCommand.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Egg;
|
||||
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckEggUpdatesCommand extends Command
|
||||
{
|
||||
protected $signature = 'p:egg:check-updates';
|
||||
|
||||
public function handle(EggExporterService $exporterService): void
|
||||
{
|
||||
$eggs = Egg::all();
|
||||
foreach ($eggs as $egg) {
|
||||
try {
|
||||
if (is_null($egg->update_url)) {
|
||||
$this->comment("{$egg->name}: Skipping (no update url set)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentJson = json_decode($exporterService->handle($egg->id));
|
||||
unset($currentJson->exported_at);
|
||||
|
||||
$updatedJson = json_decode(file_get_contents($egg->update_url));
|
||||
unset($updatedJson->exported_at);
|
||||
|
||||
if (md5(json_encode($currentJson)) === md5(json_encode($updatedJson))) {
|
||||
$this->info("{$egg->name}: Up-to-date");
|
||||
cache()->put("eggs.{$egg->uuid}.update", false, now()->addHour());
|
||||
} else {
|
||||
$this->warn("{$egg->name}: Found update");
|
||||
cache()->put("eggs.{$egg->uuid}.update", true, now()->addHour());
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$this->error("{$egg->name}: Error ({$exception->getMessage()})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,14 @@
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AppSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
protected $description = 'Configure basic environment settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:setup
|
||||
{--url= : The URL that this Panel is running on.}';
|
||||
|
||||
protected array $variables = [];
|
||||
protected $signature = 'p:environment:setup';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
@@ -30,21 +24,6 @@ class AppSettingsCommand extends Command
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
$this->variables['APP_TIMEZONE'] = 'UTC';
|
||||
|
||||
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
|
||||
'Application URL',
|
||||
config('app.url', 'https://example.com')
|
||||
);
|
||||
|
||||
// Make sure session cookies are set as "secure" when using HTTPS
|
||||
if (str_starts_with($this->variables['APP_URL'], 'https://')) {
|
||||
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
|
||||
}
|
||||
|
||||
$this->comment('Writing variables to .env file');
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
|
||||
Artisan::call('filament:optimize');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@ class DatabaseSettingsCommand extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->error('Changing the database driver will NOT move any database data!');
|
||||
$this->error('Please make sure you made a database backup first!');
|
||||
$this->error('After changing the driver you will have to manually move the old data to the new database.');
|
||||
if (!$this->confirm('Do you want to continue?')) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$selected = config('database.default', 'sqlite');
|
||||
$this->variables['DB_CONNECTION'] = $this->option('driver') ?? $this->choice(
|
||||
'Database Driver',
|
||||
|
||||
@@ -70,7 +70,7 @@ class EmailSettingsCommand extends Command
|
||||
/**
|
||||
* Handle variables for SMTP driver.
|
||||
*/
|
||||
private function setupSmtpDriverVariables()
|
||||
private function setupSmtpDriverVariables(): void
|
||||
{
|
||||
$this->variables['MAIL_HOST'] = $this->option('host') ?? $this->ask(
|
||||
trans('command/messages.environment.mail.ask_smtp_host'),
|
||||
@@ -101,7 +101,7 @@ class EmailSettingsCommand extends Command
|
||||
/**
|
||||
* Handle variables for mailgun driver.
|
||||
*/
|
||||
private function setupMailgunDriverVariables()
|
||||
private function setupMailgunDriverVariables(): void
|
||||
{
|
||||
$this->variables['MAILGUN_DOMAIN'] = $this->option('host') ?? $this->ask(
|
||||
trans('command/messages.environment.mail.ask_mailgun_domain'),
|
||||
@@ -122,7 +122,7 @@ class EmailSettingsCommand extends Command
|
||||
/**
|
||||
* Handle variables for mandrill driver.
|
||||
*/
|
||||
private function setupMandrillDriverVariables()
|
||||
private function setupMandrillDriverVariables(): void
|
||||
{
|
||||
$this->variables['MANDRILL_SECRET'] = $this->option('password') ?? $this->ask(
|
||||
trans('command/messages.environment.mail.ask_mandrill_secret'),
|
||||
@@ -133,7 +133,7 @@ class EmailSettingsCommand extends Command
|
||||
/**
|
||||
* Handle variables for postmark driver.
|
||||
*/
|
||||
private function setupPostmarkDriverVariables()
|
||||
private function setupPostmarkDriverVariables(): void
|
||||
{
|
||||
$this->variables['MAIL_DRIVER'] = 'smtp';
|
||||
$this->variables['MAIL_HOST'] = 'smtp.postmarkapp.com';
|
||||
|
||||
@@ -26,8 +26,8 @@ class InfoCommand extends Command
|
||||
{
|
||||
$this->output->title('Version Information');
|
||||
$this->table([], [
|
||||
['Panel Version', $this->versionService->versionData()['version']],
|
||||
['Latest Version', $this->versionService->getPanel()],
|
||||
['Panel Version', $this->versionService->currentPanelVersion()],
|
||||
['Latest Version', $this->versionService->latestPanelVersion()],
|
||||
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
|
||||
], 'compact');
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use Illuminate\Console\Command;
|
||||
use App\Models\Schedule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Services\Schedules\ProcessScheduleService;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ProcessRunnableCommand extends Command
|
||||
{
|
||||
@@ -24,7 +23,7 @@ class ProcessRunnableCommand extends Command
|
||||
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
|
||||
->where('is_active', true)
|
||||
->where('is_processing', false)
|
||||
->where('next_run_at', '<=', Carbon::now()->toDateTimeString())
|
||||
->where('next_run_at', '<=', now('UTC')->toDateTimeString())
|
||||
->get();
|
||||
|
||||
if ($schedules->count() < 1) {
|
||||
@@ -51,7 +50,7 @@ class ProcessRunnableCommand extends Command
|
||||
* never throw an exception out, otherwise you'll end up killing the entire run group causing
|
||||
* any other schedules to not process correctly.
|
||||
*/
|
||||
protected function processSchedule(Schedule $schedule)
|
||||
protected function processSchedule(Schedule $schedule): void
|
||||
{
|
||||
if ($schedule->tasks->isEmpty()) {
|
||||
return;
|
||||
|
||||
@@ -178,7 +178,7 @@ class UpgradeCommand extends Command
|
||||
$this->info(__('commands.upgrade.success'));
|
||||
}
|
||||
|
||||
protected function withProgress(ProgressBar $bar, \Closure $callback)
|
||||
protected function withProgress(ProgressBar $bar, \Closure $callback): void
|
||||
{
|
||||
$bar->clear();
|
||||
$callback();
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Console\Commands\Egg\CheckEggUpdatesCommand;
|
||||
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneImagesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
|
||||
use App\Console\Commands\Schedule\ProcessRunnableCommand;
|
||||
use App\Jobs\NodeStatistics;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Webhook;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Database\Console\PruneCommand;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use App\Console\Commands\Schedule\ProcessRunnableCommand;
|
||||
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
|
||||
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneImagesCommand;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
@@ -35,6 +37,7 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
|
||||
$schedule->command(PruneImagesCommand::class)->daily();
|
||||
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
|
||||
|
||||
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
|
||||
|
||||
@@ -46,5 +49,9 @@ class Kernel extends ConsoleKernel
|
||||
if (config('activity.prune_days')) {
|
||||
$schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily();
|
||||
}
|
||||
|
||||
if (config('panel.webhook.prune_days')) {
|
||||
$schedule->command(PruneCommand::class, ['--model' => [Webhook::class]])->daily();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class FailedCaptcha extends Event
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public string $ip, public string $domain)
|
||||
public function __construct(public string $ip, public ?string $message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Created extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Creating extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleting extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Saved extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Saving extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Updated extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Updating extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Subuser;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Created extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Subuser $subuser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Subuser;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Creating extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Subuser $subuser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Subuser;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Subuser $subuser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Subuser;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleting extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Subuser $subuser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Created extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Creating extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleting extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class AccountNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class AutoDeploymentException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -4,18 +4,22 @@ namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Container\Container;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
{
|
||||
public const LEVEL_DEBUG = 'debug';
|
||||
|
||||
public const LEVEL_INFO = 'info';
|
||||
|
||||
public const LEVEL_WARNING = 'warning';
|
||||
|
||||
public const LEVEL_ERROR = 'error';
|
||||
|
||||
/**
|
||||
@@ -46,7 +50,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
* and then redirecting them back to the page that they came from. If the
|
||||
* request originated from an API hit, return the error in JSONAPI spec format.
|
||||
*/
|
||||
public function render(Request $request)
|
||||
public function render(Request $request): bool|RedirectResponse|JsonResponse
|
||||
{
|
||||
if ($request->is('livewire/update')) {
|
||||
Notification::make()
|
||||
@@ -55,15 +59,13 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
|
||||
}
|
||||
|
||||
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
|
||||
|
||||
return redirect()->back()->withInput();
|
||||
}
|
||||
|
||||
@@ -73,10 +75,10 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function report()
|
||||
public function report(): void
|
||||
{
|
||||
if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -85,6 +87,6 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
throw $this->getPrevious();
|
||||
}
|
||||
|
||||
return $logger->{$this->getErrorLevel()}($this->getPrevious());
|
||||
$logger->{$this->getErrorLevel()}($this->getPrevious());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
@@ -140,7 +140,7 @@ class Handler extends ExceptionHandler
|
||||
* Transform a validation exception into a consistent format to be returned for
|
||||
* calls to the API.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function invalidJson($request, ValidationException $exception): JsonResponse
|
||||
{
|
||||
@@ -236,7 +236,7 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Convert an authentication exception into an unauthenticated response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse
|
||||
{
|
||||
@@ -273,6 +273,7 @@ class Handler extends ExceptionHandler
|
||||
*/
|
||||
public static function toArray(\Throwable $e): array
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
return (new self(app()))->convertExceptionToArray($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ use GuzzleHttp\Exception\GuzzleException;
|
||||
use App\Exceptions\DisplayException;
|
||||
use Illuminate\Support\Facades\Context;
|
||||
|
||||
/**
|
||||
* @method \GuzzleHttp\Exception\GuzzleException getPrevious()
|
||||
*/
|
||||
class DaemonConnectionException extends DisplayException
|
||||
{
|
||||
private int $statusCode = Response::HTTP_GATEWAY_TIMEOUT;
|
||||
|
||||
@@ -10,7 +10,7 @@ class HttpForbiddenException extends HttpException
|
||||
/**
|
||||
* HttpForbiddenException constructor.
|
||||
*/
|
||||
public function __construct(string $message = null, \Throwable $previous = null)
|
||||
public function __construct(?string $message = null, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Http\Server;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class FileTypeNotEditableException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class ServerStateConflictException extends ConflictHttpException
|
||||
* Exception thrown when the server is in an unsupported state for API access or
|
||||
* certain operations within the codebase.
|
||||
*/
|
||||
public function __construct(Server $server, \Throwable $previous = null)
|
||||
public function __construct(Server $server, ?\Throwable $previous = null)
|
||||
{
|
||||
$message = 'This server is currently in an unsupported state, please try again later.';
|
||||
if ($server->isSuspended()) {
|
||||
|
||||
@@ -11,7 +11,7 @@ class TwoFactorAuthRequiredException extends HttpException implements HttpExcept
|
||||
/**
|
||||
* TwoFactorAuthRequiredException constructor.
|
||||
*/
|
||||
public function __construct(\Throwable $previous = null)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Repository\Daemon;
|
||||
|
||||
use App\Exceptions\Repository\RepositoryException;
|
||||
|
||||
class InvalidPowerSignalException extends RepositoryException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Repository;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class RepositoryException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class AllocationDoesNotBelongToServerException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Egg;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class BadJsonFormatException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Egg;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoParentConfigurationFoundException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Schedule\Task;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class TaskIntervalTooLongException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Server;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class RequiredVariableMissingException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class ServiceLimitExceededException extends DisplayException
|
||||
* Exception thrown when something goes over a defined limit, such as allocated
|
||||
* ports, tasks, databases, etc.
|
||||
*/
|
||||
public function __construct(string $message, \Throwable $previous = null)
|
||||
public function __construct(string $message, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $previous, self::LEVEL_WARNING);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Exceptions\DisplayException;
|
||||
class TwoFactorAuthenticationTokenInvalid extends DisplayException
|
||||
{
|
||||
public string $title = 'Invalid 2FA Code';
|
||||
|
||||
public string $icon = 'tabler-2fa';
|
||||
|
||||
public function __construct()
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Transformer;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class InvalidTransformerLevelException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class BackupManager
|
||||
/**
|
||||
* Returns a backup adapter instance.
|
||||
*/
|
||||
public function adapter(string $name = null): FilesystemAdapter
|
||||
public function adapter(?string $name = null): FilesystemAdapter
|
||||
{
|
||||
return $this->get($name ?: $this->getDefaultAdapter());
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class BackupManager
|
||||
/**
|
||||
* Unset the given adapter instances.
|
||||
*
|
||||
* @param string|string[] $adapter
|
||||
* @param string|string[] $adapter
|
||||
*/
|
||||
public function forget(array|string $adapter): self
|
||||
{
|
||||
|
||||
@@ -7,7 +7,9 @@ use App\Models\DatabaseHost;
|
||||
class DynamicDatabaseConnection
|
||||
{
|
||||
public const DB_CHARSET = 'utf8';
|
||||
|
||||
public const DB_COLLATION = 'utf8_unicode_ci';
|
||||
|
||||
public const DB_DRIVER = 'mysql';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Theme extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'extensions.themes';
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Themes;
|
||||
|
||||
class Theme
|
||||
{
|
||||
public function js($path): string
|
||||
{
|
||||
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
|
||||
}
|
||||
|
||||
public function css($path): string
|
||||
{
|
||||
return sprintf('<link media="all" type="text/css" rel="stylesheet" href="%s"/>' . PHP_EOL, $this->getUrl($path));
|
||||
}
|
||||
|
||||
protected function getUrl($path): string
|
||||
{
|
||||
return '/themes/panel/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use App\Services\Activity\ActivityLogBatchService;
|
||||
|
||||
class LogBatch extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return ActivityLogBatchService::class;
|
||||
}
|
||||
}
|
||||
36
app/Filament/Pages/Auth/Login.php
Normal file
36
app/Filament/Pages/Auth/Login.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
|
||||
use Filament\Pages\Auth\Login as BaseLogin;
|
||||
|
||||
class Login extends BaseLogin
|
||||
{
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->form(
|
||||
$this->makeForm()
|
||||
->schema([
|
||||
$this->getEmailFormComponent(),
|
||||
$this->getPasswordFormComponent(),
|
||||
$this->getRememberFormComponent(),
|
||||
Turnstile::make('captcha')
|
||||
->hidden(!config('turnstile.turnstile_enabled'))
|
||||
->validationMessages([
|
||||
'required' => config('turnstile.error_messages.turnstile_check_message'),
|
||||
]),
|
||||
])
|
||||
->statePath('data'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected function throwFailureValidationException(): never
|
||||
{
|
||||
$this->dispatch('reset-captcha');
|
||||
|
||||
parent::throwFailureValidationException();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource\Pages\CreateNode;
|
||||
use App\Filament\Resources\NodeResource\Pages\ListNodes;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
@@ -28,16 +29,20 @@ class Dashboard extends Page
|
||||
|
||||
public string $activeTab = 'nodes';
|
||||
|
||||
private SoftwareVersionService $softwareVersionService;
|
||||
|
||||
public function mount(SoftwareVersionService $softwareVersionService): void
|
||||
{
|
||||
$this->softwareVersionService = $softwareVersionService;
|
||||
}
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
/** @var SoftwareVersionService $softwareVersionService */
|
||||
$softwareVersionService = app(SoftwareVersionService::class);
|
||||
|
||||
return [
|
||||
'inDevelopment' => config('app.version') === 'canary',
|
||||
'version' => $softwareVersionService->versionData()['version'],
|
||||
'latestVersion' => $softwareVersionService->getPanel(),
|
||||
'isLatest' => $softwareVersionService->isLatestPanel(),
|
||||
'version' => $this->softwareVersionService->currentPanelVersion(),
|
||||
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
|
||||
'isLatest' => $this->softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
@@ -61,13 +66,13 @@ class Dashboard extends Page
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
|
||||
->icon('tabler-server-2')
|
||||
->url(route('filament.admin.resources.nodes.create')),
|
||||
->url(CreateNode::getUrl()),
|
||||
],
|
||||
'supportActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url($softwareVersionService->getDonations(), true)
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
|
||||
@@ -2,25 +2,29 @@
|
||||
|
||||
namespace App\Filament\Pages\Installer;
|
||||
|
||||
use App\Filament\Pages\Installer\Steps\AdminUserStep;
|
||||
use App\Filament\Pages\Dashboard;
|
||||
use App\Filament\Pages\Installer\Steps\CacheStep;
|
||||
use App\Filament\Pages\Installer\Steps\DatabaseStep;
|
||||
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
|
||||
use App\Filament\Pages\Installer\Steps\RedisStep;
|
||||
use App\Filament\Pages\Installer\Steps\QueueStep;
|
||||
use App\Filament\Pages\Installer\Steps\RequirementsStep;
|
||||
use App\Filament\Pages\Installer\Steps\SessionStep;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\CheckMigrationsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Wizard;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\SimplePage;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
@@ -32,10 +36,9 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
{
|
||||
use CheckMigrationsTrait;
|
||||
use EnvironmentWriterTrait;
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithForms;
|
||||
|
||||
public $data = [];
|
||||
public array $data = [];
|
||||
|
||||
protected static string $view = 'filament.pages.installer';
|
||||
|
||||
@@ -44,44 +47,32 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
}
|
||||
|
||||
public static function show(): bool
|
||||
public static function isInstalled(): bool
|
||||
{
|
||||
if (User::count() <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config('panel.client_features.installer.enabled')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// This defaults to true so existing panels count as "installed"
|
||||
return env('APP_INSTALLED', true);
|
||||
}
|
||||
|
||||
public function mount()
|
||||
public function mount(): void
|
||||
{
|
||||
abort_unless(self::show(), 404);
|
||||
abort_if(self::isInstalled(), 404);
|
||||
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function dehydrate(): void
|
||||
{
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('cache:clear');
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Wizard::make([
|
||||
RequirementsStep::make(),
|
||||
EnvironmentStep::make(),
|
||||
DatabaseStep::make(),
|
||||
RedisStep::make()
|
||||
->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'),
|
||||
AdminUserStep::make(),
|
||||
EnvironmentStep::make($this),
|
||||
DatabaseStep::make($this),
|
||||
CacheStep::make($this),
|
||||
QueueStep::make($this),
|
||||
SessionStep::make(),
|
||||
])
|
||||
->persistStepInQueryString()
|
||||
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
@@ -100,61 +91,94 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
public function submit(UserCreationService $userCreationService): Redirector|RedirectResponse
|
||||
{
|
||||
return true;
|
||||
// Disable installer
|
||||
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
|
||||
|
||||
// Create admin user & login
|
||||
$user = $this->createAdminUser($userCreationService);
|
||||
auth()->guard()->login($user, true);
|
||||
|
||||
// Write session data at the very end to avoid "page expired" errors
|
||||
$this->writeToEnv('env_session');
|
||||
|
||||
// Redirect to admin panel
|
||||
return redirect(Dashboard::getUrl());
|
||||
}
|
||||
|
||||
public function submit()
|
||||
public function writeToEnv(string $key): void
|
||||
{
|
||||
try {
|
||||
$inputs = $this->form->getState();
|
||||
|
||||
// Write variables to .env file
|
||||
$variables = array_get($inputs, 'env');
|
||||
$variables = array_get($this->data, $key);
|
||||
$variables = array_filter($variables); // Filter array to remove NULL values
|
||||
$this->writeToEnvironment($variables);
|
||||
|
||||
// Clear config cache
|
||||
Artisan::call('config:clear');
|
||||
|
||||
// Run migrations
|
||||
Artisan::call('migrate', [
|
||||
'--force' => true,
|
||||
'--seed' => true,
|
||||
'--database' => $variables['DB_CONNECTION'],
|
||||
]);
|
||||
|
||||
if (!$this->hasCompletedMigrations()) {
|
||||
throw new Exception('Migrations didn\'t run successfully. Double check your database configuration.');
|
||||
}
|
||||
|
||||
// Create first admin user
|
||||
$userData = array_get($inputs, 'user');
|
||||
$userData['root_admin'] = true;
|
||||
$user = app(UserCreationService::class)->handle($userData);
|
||||
|
||||
// Install setup complete
|
||||
$this->writeToEnvironment(['APP_INSTALLER' => 'false']);
|
||||
|
||||
$this->rememberData();
|
||||
|
||||
Notification::make()
|
||||
->title('Successfully Installed')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
auth()->loginUsingId($user->id);
|
||||
|
||||
return redirect('/admin');
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Installation Failed')
|
||||
->title('Could not write to .env file')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while writing .env file');
|
||||
}
|
||||
|
||||
Artisan::call('config:clear');
|
||||
}
|
||||
|
||||
public function runMigrations(string $driver): void
|
||||
{
|
||||
try {
|
||||
Artisan::call('migrate', [
|
||||
'--force' => true,
|
||||
'--seed' => true,
|
||||
'--database' => $driver,
|
||||
]);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Migrations failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while running migrations');
|
||||
}
|
||||
|
||||
if (!$this->hasCompletedMigrations()) {
|
||||
Notification::make()
|
||||
->title('Migrations failed')
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Migrations failed');
|
||||
}
|
||||
}
|
||||
|
||||
public function createAdminUser(UserCreationService $userCreationService): User
|
||||
{
|
||||
try {
|
||||
$userData = array_get($this->data, 'user');
|
||||
$userData['root_admin'] = true;
|
||||
|
||||
return $userCreationService->handle($userData);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Could not create admin user')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while creating admin user');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
|
||||
class AdminUserStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('user')
|
||||
->label('Admin User')
|
||||
->schema([
|
||||
TextInput::make('user.email')
|
||||
->label('Admin E-Mail')
|
||||
->required()
|
||||
->email()
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Admin Username')
|
||||
->required()
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Admin Password')
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
123
app/Filament/Pages/Installer/Steps/CacheStep.php
Normal file
123
app/Filament/Pages/Installer/Steps/CacheStep.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Redis\RedisManager;
|
||||
|
||||
class CacheStep
|
||||
{
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('cache')
|
||||
->label('Cache')
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_cache.CACHE_STORE')
|
||||
->label('Cache Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::CACHE_DRIVERS)
|
||||
->default(config('cache.default'))
|
||||
->columnSpanFull()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
if ($state !== 'redis') {
|
||||
$set('env_cache.REDIS_HOST', null);
|
||||
$set('env_cache.REDIS_PORT', null);
|
||||
$set('env_cache.REDIS_USERNAME', null);
|
||||
$set('env_cache.REDIS_PASSWORD', null);
|
||||
} else {
|
||||
$set('env_cache.REDIS_HOST', $get('env_cache.REDIS_HOST') ?? '127.0.0.1');
|
||||
$set('env_cache.REDIS_PORT', $get('env_cache.REDIS_PORT') ?? '6379');
|
||||
$set('env_cache.REDIS_USERNAME', null);
|
||||
}
|
||||
}),
|
||||
TextInput::make('env_cache.REDIS_HOST')
|
||||
->label('Redis Host')
|
||||
->placeholder('127.0.0.1')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
|
||||
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.host') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_PORT')
|
||||
->label('Redis Port')
|
||||
->placeholder('6379')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your redis server.')
|
||||
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.port') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_USERNAME')
|
||||
->label('Redis Username')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your redis user. Can be empty')
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.username') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_PASSWORD')
|
||||
->label('Redis Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password for your redis user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.password') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
])
|
||||
->afterValidation(function (Get $get, Application $app) use ($installer) {
|
||||
$driver = $get('env_cache.CACHE_STORE');
|
||||
|
||||
if (!self::testConnection($app, $driver, $get('env_cache.REDIS_HOST'), $get('env_cache.REDIS_PORT'), $get('env_cache.REDIS_USERNAME'), $get('env_cache.REDIS_PASSWORD'))) {
|
||||
throw new Halt('Redis connection failed');
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_cache');
|
||||
});
|
||||
}
|
||||
|
||||
private static function testConnection(Application $app, string $driver, ?string $host, null|string|int $port, ?string $username, ?string $password): bool
|
||||
{
|
||||
if ($driver !== 'redis') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$redis = new RedisManager($app, 'predis', [
|
||||
'default' => [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
],
|
||||
]);
|
||||
|
||||
$redis->connection()->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,91 +2,138 @@
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PDOException;
|
||||
|
||||
class DatabaseStep
|
||||
{
|
||||
public static function make(): Step
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('database')
|
||||
->label('Database')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->columnSpanFull()
|
||||
ToggleButtons::make('env_database.DB_CONNECTION')
|
||||
->label('Database Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
|
||||
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
|
||||
->required()
|
||||
->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
|
||||
TextInput::make('env.DB_HOST')
|
||||
->inline()
|
||||
->options(self::DATABASE_DRIVERS)
|
||||
->default(config('database.default'))
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
$set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel');
|
||||
|
||||
if ($state === 'sqlite') {
|
||||
$set('env_database.DB_HOST', null);
|
||||
$set('env_database.DB_PORT', null);
|
||||
$set('env_database.DB_USERNAME', null);
|
||||
$set('env_database.DB_PASSWORD', null);
|
||||
} else {
|
||||
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
|
||||
$set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306');
|
||||
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
|
||||
}
|
||||
}),
|
||||
TextInput::make('env_database.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->placeholder(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
|
||||
->required()
|
||||
->default('database.sqlite'),
|
||||
TextInput::make('env_database.DB_HOST')
|
||||
->label('Database Host')
|
||||
->placeholder('127.0.0.1')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your database. Make sure it is reachable.')
|
||||
->required()
|
||||
->default(env('DB_HOST', '127.0.0.1'))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_PORT')
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PORT')
|
||||
->label('Database Port')
|
||||
->placeholder('3306')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your database.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('DB_PORT', 3306))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_USERNAME')
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_USERNAME')
|
||||
->label('Database Username')
|
||||
->placeholder('pelican')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your database user.')
|
||||
->required()
|
||||
->default(env('DB_USERNAME', 'pelican'))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_PASSWORD')
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PASSWORD')
|
||||
->label('Database Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password of your database user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(env('DB_PASSWORD'))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
])
|
||||
->afterValidation(function (Get $get) {
|
||||
$driver = $get('env.DB_CONNECTION');
|
||||
if ($driver !== 'sqlite') {
|
||||
try {
|
||||
config()->set('database.connections._panel_install_test', [
|
||||
'driver' => $driver,
|
||||
'host' => $get('env.DB_HOST'),
|
||||
'port' => $get('env.DB_PORT'),
|
||||
'database' => $get('env.DB_DATABASE'),
|
||||
'username' => $get('env.DB_USERNAME'),
|
||||
'password' => $get('env.DB_PASSWORD'),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
->afterValidation(function (Get $get) use ($installer) {
|
||||
$driver = $get('env_database.DB_CONNECTION');
|
||||
|
||||
DB::connection('_panel_install_test')->getPdo();
|
||||
} catch (PDOException $exception) {
|
||||
Notification::make()
|
||||
->title('Database connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
DB::disconnect('_panel_install_test');
|
||||
|
||||
throw new Halt('Database connection failed');
|
||||
}
|
||||
if (!self::testConnection($driver, $get('env_database.DB_HOST'), $get('env_database.DB_PORT'), $get('env_database.DB_DATABASE'), $get('env_database.DB_USERNAME'), $get('env_database.DB_PASSWORD'))) {
|
||||
throw new Halt('Database connection failed');
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_database');
|
||||
|
||||
$installer->runMigrations($driver);
|
||||
});
|
||||
}
|
||||
|
||||
private static function testConnection(string $driver, ?string $host, null|string|int $port, ?string $database, ?string $username, ?string $password): bool
|
||||
{
|
||||
if ($driver === 'sqlite') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
config()->set('database.connections._panel_install_test', [
|
||||
'driver' => $driver,
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'database' => $database,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
DB::connection('_panel_install_test')->getPdo();
|
||||
} catch (Exception $exception) {
|
||||
DB::disconnect('_panel_install_test');
|
||||
|
||||
Notification::make()
|
||||
->title('Database connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,93 +2,51 @@
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Set;
|
||||
|
||||
class EnvironmentStep
|
||||
{
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'sync' => 'Sync',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
public static function make(): Step
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('environment')
|
||||
->label('Environment')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.APP_NAME')
|
||||
TextInput::make('env_general.APP_NAME')
|
||||
->label('App Name')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the Name of your Panel.')
|
||||
->required()
|
||||
->default(config('app.name')),
|
||||
TextInput::make('env.APP_URL')
|
||||
TextInput::make('env_general.APP_URL')
|
||||
->label('App URL')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the URL you access your Panel from.')
|
||||
->required()
|
||||
->default(config('app.url'))
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))),
|
||||
Toggle::make('env.SESSION_SECURE_COOKIE')
|
||||
->hidden()
|
||||
->default(env('SESSION_SECURE_COOKIE')),
|
||||
ToggleButtons::make('env.CACHE_STORE')
|
||||
->label('Cache Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::CACHE_DRIVERS)
|
||||
->default(config('cache.default', 'file')),
|
||||
ToggleButtons::make('env.SESSION_DRIVER')
|
||||
->label('Session Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::SESSION_DRIVERS)
|
||||
->default(config('session.driver', 'file')),
|
||||
ToggleButtons::make('env.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Sync" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::QUEUE_DRIVERS)
|
||||
->default(config('queue.default', 'database')),
|
||||
ToggleButtons::make('env.DB_CONNECTION')
|
||||
->label('Database Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::DATABASE_DRIVERS)
|
||||
->default(config('database.default', 'sqlite')),
|
||||
]);
|
||||
->default(url('')),
|
||||
Fieldset::make('adminuser')
|
||||
->label('Admin User')
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextInput::make('user.email')
|
||||
->label('E-Mail')
|
||||
->required()
|
||||
->email()
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Username')
|
||||
->required()
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Password')
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
]),
|
||||
])
|
||||
->afterValidation(fn () => $installer->writeToEnv('env_general'));
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Filament/Pages/Installer/Steps/QueueStep.php
Normal file
64
app/Filament/Pages/Installer/Steps/QueueStep.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class QueueStep
|
||||
{
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Sync',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('queue')
|
||||
->label('Queue')
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_queue.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::QUEUE_DRIVERS)
|
||||
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
|
||||
->default(config('queue.default')),
|
||||
Toggle::make('done')
|
||||
->label('I have done both steps below.')
|
||||
->accepted(fn () => !file_exists('/.dockerenv'))
|
||||
->inline(false)
|
||||
->validationMessages([
|
||||
'accepted' => 'You need to do both steps before continuing!',
|
||||
])
|
||||
->hidden(fn () => file_exists('/.dockerenv')),
|
||||
TextInput::make('crontab')
|
||||
->label(new HtmlString('Run the following command to set up your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
|
||||
->disabled()
|
||||
->hintAction(CopyAction::make())
|
||||
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -')
|
||||
->hidden(fn () => file_exists('/.dockerenv'))
|
||||
->columnSpanFull(),
|
||||
TextInput::make('queueService')
|
||||
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
|
||||
->disabled()
|
||||
->hintAction(CopyAction::make())
|
||||
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service')
|
||||
->hidden(fn () => file_exists('/.dockerenv'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->afterValidation(function () use ($installer) {
|
||||
$installer->writeToEnv('env_queue');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class RedisStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('redis')
|
||||
->label('Redis')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.REDIS_HOST')
|
||||
->label('Redis Host')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
|
||||
->required()
|
||||
->default(config('database.redis.default.host')),
|
||||
TextInput::make('env.REDIS_PORT')
|
||||
->label('Redis Port')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your redis server.')
|
||||
->required()
|
||||
->default(config('database.redis.default.port')),
|
||||
TextInput::make('env.REDIS_USERNAME')
|
||||
->label('Redis Username')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your redis user. Can be empty')
|
||||
->default(config('database.redis.default.username')),
|
||||
TextInput::make('env.REDIS_PASSWORD')
|
||||
->label('Redis Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password for your redis user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(config('database.redis.default.password')),
|
||||
])
|
||||
->afterValidation(function (Get $get) {
|
||||
try {
|
||||
config()->set('database.redis._panel_install_test', [
|
||||
'host' => $get('env.REDIS_HOST'),
|
||||
'username' => $get('env.REDIS_USERNAME'),
|
||||
'password' => $get('env.REDIS_PASSWORD'),
|
||||
'port' => $get('env.REDIS_PORT'),
|
||||
]);
|
||||
|
||||
Redis::connection('_panel_install_test')->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt('Redis connection failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,20 @@ use Filament\Support\Exceptions\Halt;
|
||||
|
||||
class RequirementsStep
|
||||
{
|
||||
public const MIN_PHP_VERSION = '8.2';
|
||||
|
||||
public static function make(): Step
|
||||
{
|
||||
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0;
|
||||
$correctPhpVersion = version_compare(PHP_VERSION, self::MIN_PHP_VERSION) >= 0;
|
||||
|
||||
$fields = [
|
||||
Section::make('PHP Version')
|
||||
->description('8.2 or newer')
|
||||
->description(self::MIN_PHP_VERSION . ' or newer')
|
||||
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctPhpVersion ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'),
|
||||
->content('Your PHP Version is ' . PHP_VERSION . '.'),
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -80,7 +82,7 @@ class RequirementsStep
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt();
|
||||
throw new Halt('Some requirements are missing');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
38
app/Filament/Pages/Installer/Steps/SessionStep.php
Normal file
38
app/Filament/Pages/Installer/Steps/SessionStep.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
|
||||
class SessionStep
|
||||
{
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('session')
|
||||
->label('Session')
|
||||
->schema([
|
||||
ToggleButtons::make('env_session.SESSION_DRIVER')
|
||||
->label('Session Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::SESSION_DRIVERS)
|
||||
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
|
||||
->default(config('session.driver')),
|
||||
TextInput::make('env_session.SESSION_SECURE_COOKIE')
|
||||
->hidden()
|
||||
->default(request()->isSecure()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
@@ -24,8 +25,11 @@ use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
@@ -38,6 +42,7 @@ class Settings extends Page implements HasForms
|
||||
use InteractsWithHeaderActions;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static string $view = 'filament.pages.settings';
|
||||
@@ -66,10 +71,11 @@ class Settings extends Page implements HasForms
|
||||
->label('General')
|
||||
->icon('tabler-home')
|
||||
->schema($this->generalSettings()),
|
||||
Tab::make('recaptcha')
|
||||
->label('reCAPTCHA')
|
||||
Tab::make('captcha')
|
||||
->label('Captcha')
|
||||
->icon('tabler-shield')
|
||||
->schema($this->recaptchaSettings()),
|
||||
->schema($this->captchaSettings())
|
||||
->columns(3),
|
||||
Tab::make('mail')
|
||||
->label('Mail')
|
||||
->icon('tabler-mail')
|
||||
@@ -92,7 +98,6 @@ class Settings extends Page implements HasForms
|
||||
TextInput::make('APP_NAME')
|
||||
->label('App Name')
|
||||
->required()
|
||||
->alphaNum()
|
||||
->default(env('APP_NAME', 'Pelican')),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label('App Favicon')
|
||||
@@ -146,7 +151,7 @@ class Settings extends Page implements HasForms
|
||||
->separator()
|
||||
->splitKeys(['Tab', ' '])
|
||||
->placeholder('New IP or IP Range')
|
||||
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
|
||||
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
|
||||
->hintActions([
|
||||
FormAction::make('clear')
|
||||
->label('Clear')
|
||||
@@ -159,56 +164,71 @@ class Settings extends Page implements HasForms
|
||||
->label('Set to Cloudflare IPs')
|
||||
->icon('tabler-brand-cloudflare')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
|
||||
'173.245.48.0/20',
|
||||
'103.21.244.0/22',
|
||||
'103.22.200.0/22',
|
||||
'103.31.4.0/22',
|
||||
'141.101.64.0/18',
|
||||
'108.162.192.0/18',
|
||||
'190.93.240.0/20',
|
||||
'188.114.96.0/20',
|
||||
'197.234.240.0/22',
|
||||
'198.41.128.0/17',
|
||||
'162.158.0.0/15',
|
||||
'104.16.0.0/13',
|
||||
'104.24.0.0/14',
|
||||
'172.64.0.0/13',
|
||||
'131.0.72.0/22',
|
||||
])),
|
||||
->action(function (Client $client, Set $set) {
|
||||
$ips = collect();
|
||||
try {
|
||||
$response = $client->request(
|
||||
'GET',
|
||||
'https://api.cloudflare.com/client/v4/ips',
|
||||
config('panel.guzzle')
|
||||
);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$result = json_decode($response->getBody(), true)['result'];
|
||||
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
|
||||
$ips->push(...data_get($result, $value));
|
||||
}
|
||||
$ips->unique();
|
||||
}
|
||||
} catch (GuzzleException $e) {
|
||||
}
|
||||
|
||||
$set('TRUSTED_PROXIES', $ips->values()->all());
|
||||
}),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function recaptchaSettings(): array
|
||||
private function captchaSettings(): array
|
||||
{
|
||||
return [
|
||||
Toggle::make('RECAPTCHA_ENABLED')
|
||||
->label('Enable reCAPTCHA?')
|
||||
Toggle::make('TURNSTILE_ENABLED')
|
||||
->label('Enable Turnstile Captcha?')
|
||||
->inline(false)
|
||||
->columnSpan(1)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
|
||||
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
|
||||
TextInput::make('RECAPTCHA_DOMAIN')
|
||||
->label('Domain')
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
|
||||
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
|
||||
Placeholder::make('info')
|
||||
->columnSpan(2)
|
||||
->content(new HtmlString('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
|
||||
TextInput::make('TURNSTILE_SITE_KEY')
|
||||
->label('Site Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
|
||||
TextInput::make('RECAPTCHA_WEBSITE_KEY')
|
||||
->label('Website Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
|
||||
TextInput::make('RECAPTCHA_SECRET_KEY')
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
|
||||
->placeholder('1x00000000000000000000AA'),
|
||||
TextInput::make('TURNSTILE_SECRET_KEY')
|
||||
->label('Secret Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key')))
|
||||
->placeholder('1x0000000000000000000000000000000AA'),
|
||||
Toggle::make('TURNSTILE_VERIFY_DOMAIN')
|
||||
->label('Verify domain?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state))
|
||||
->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -522,6 +542,39 @@ class Settings extends Page implements HasForms
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
|
||||
]),
|
||||
Section::make('Server')
|
||||
->description('Settings for Servers.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_EDITABLE_SERVER_DESCRIPTIONS')
|
||||
->label('Allow Users to edit Server Descriptions?')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
|
||||
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
|
||||
]),
|
||||
Section::make('Webhook')
|
||||
->description('Configure how often old webhook logs should be pruned.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_WEBHOOK_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->default(env('APP_WEBHOOK_PRUNE_DAYS', config('panel.webhook.prune_days'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,31 +5,28 @@ namespace App\Filament\Resources;
|
||||
use App\Filament\Resources\ApiKeyResource\Pages;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ApiKeyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ApiKey::class;
|
||||
|
||||
protected static ?string $label = 'API Key';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-key';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::where('key_type', '2')->count() ?: null;
|
||||
return static::getModel()::where('key_type', ApiKey::TYPE_APPLICATION)->count() ?: null;
|
||||
}
|
||||
|
||||
public static function canEdit($record): bool
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -11,6 +11,7 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CreateApiKey extends CreateRecord
|
||||
{
|
||||
@@ -41,7 +42,7 @@ class CreateApiKey extends CreateRecord
|
||||
'md' => 2,
|
||||
])
|
||||
->schema(
|
||||
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
|
||||
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
|
||||
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||
->options([
|
||||
0 => 'None',
|
||||
@@ -87,4 +88,20 @@ class CreateApiKey extends CreateRecord
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$permissions = [];
|
||||
|
||||
foreach (ApiKey::getPermissionList() as $permission) {
|
||||
if (isset($data['permissions_' . $permission])) {
|
||||
$permissions[$permission] = intval($data['permissions_' . $permission]);
|
||||
unset($data['permissions_' . $permission]);
|
||||
}
|
||||
}
|
||||
|
||||
$data['permissions'] = $permissions;
|
||||
|
||||
return parent::handleRecordCreation($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Filament\Resources\ApiKeyResource;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
@@ -51,13 +52,23 @@ class ListApiKeys extends ListRecords
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-key')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No API Keys')
|
||||
->emptyStateActions([
|
||||
CreateAction::make('create')
|
||||
->label('Create API Key')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
Actions\CreateAction::make()
|
||||
->label('Create API Key')
|
||||
->hidden(fn () => ApiKey::where('key_type', ApiKey::TYPE_APPLICATION)->count() <= 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ class DatabaseHostResource extends Resource
|
||||
{
|
||||
protected static ?string $model = DatabaseHost::class;
|
||||
|
||||
protected static ?string $label = 'Databases';
|
||||
protected static ?string $label = 'Database Host';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -20,13 +21,6 @@ class DatabaseHostResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -4,18 +4,22 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Services\Databases\Hosts\HostCreationService;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDOException;
|
||||
|
||||
class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
private HostCreationService $service;
|
||||
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected ?string $heading = 'Database Hosts';
|
||||
@@ -24,6 +28,11 @@ class CreateDatabaseHost extends CreateRecord
|
||||
|
||||
protected ?string $subheading = '(database servers that can have individual databases)';
|
||||
|
||||
public function boot(HostCreationService $service): void
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -94,10 +103,10 @@ class CreateDatabaseHost extends CreateRecord
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
return resolve(HostCreationService::class)->handle($data);
|
||||
return $this->service->handle($data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
public function exception(Exception $e, Closure $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof PDOException) {
|
||||
Notification::make()
|
||||
|
||||
@@ -6,14 +6,16 @@ use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Services\Databases\Hosts\HostUpdateService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDOException;
|
||||
|
||||
@@ -21,6 +23,13 @@ class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
private HostUpdateService $hostUpdateService;
|
||||
|
||||
public function boot(HostUpdateService $hostUpdateService): void
|
||||
{
|
||||
$this->hostUpdateService = $hostUpdateService;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -97,12 +106,16 @@ class EditDatabaseHost extends EditRecord
|
||||
];
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate($record, array $data): Model
|
||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||
{
|
||||
return resolve(HostUpdateService::class)->handle($record->id, $data);
|
||||
if (!$record instanceof DatabaseHost) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
return $this->hostUpdateService->handle($record, $data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
public function exception(Exception $e, Closure $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof PDOException) {
|
||||
Notification::make()
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Models\DatabaseHost;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
@@ -30,13 +32,16 @@ class ListDatabaseHosts extends ListRecords
|
||||
->sortable(),
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('max_databases')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
->icon('tabler-database')
|
||||
->label('Databases'),
|
||||
TextColumn::make('node.name')
|
||||
->numeric()
|
||||
->icon('tabler-server-2')
|
||||
->placeholder('No Nodes')
|
||||
->sortable(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
])
|
||||
@@ -45,13 +50,23 @@ class ListDatabaseHosts extends ListRecords
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete databasehost')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-database')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Database Hosts')
|
||||
->emptyStateActions([
|
||||
CreateAction::make('create')
|
||||
->label('Create Database Host')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make('create')->label('New Database Host'),
|
||||
Actions\CreateAction::make('create')
|
||||
->label('Create Database Host')
|
||||
->hidden(fn () => DatabaseHost::count() <= 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
@@ -40,6 +41,7 @@ class DatabasesRelationManager extends RelationManager
|
||||
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
@@ -60,7 +62,7 @@ class DatabasesRelationManager extends RelationManager
|
||||
]);
|
||||
}
|
||||
|
||||
protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void
|
||||
protected function rotatePassword(DatabasePasswordService $service, Database $database, Set $set, Get $get): void
|
||||
{
|
||||
$newPassword = $service->handle($database);
|
||||
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database');
|
||||
|
||||
@@ -13,6 +13,7 @@ class DatabaseResource extends Resource
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -20,13 +21,6 @@ class DatabaseResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -21,9 +21,12 @@ class CreateDatabase extends CreateRecord
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
TextInput::make('database_host_id')
|
||||
->required()
|
||||
->numeric(),
|
||||
Select::make('database_host_id')
|
||||
->relationship('host', 'name')
|
||||
->searchable()
|
||||
->selectablePlaceholder(false)
|
||||
->preload()
|
||||
->required(),
|
||||
TextInput::make('database')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
@@ -21,13 +21,6 @@ class EggResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getGloballySearchableAttributes(): array
|
||||
{
|
||||
return ['name', 'tags', 'uuid', 'id'];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Filament\Resources\EggResource;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
@@ -15,10 +16,9 @@ use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -27,6 +27,7 @@ class CreateEgg extends CreateRecord
|
||||
protected static string $resource = EggResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -155,7 +156,7 @@ class CreateEgg extends CreateRecord
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->required(),
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
@@ -26,6 +25,7 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
@@ -40,6 +40,7 @@ class EditEgg extends EditRecord
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('Configuration')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->icon('tabler-egg')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
@@ -80,6 +81,7 @@ class EditEgg extends EditRecord
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->inline(false)
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
@@ -105,9 +107,9 @@ class EditEgg extends EditRecord
|
||||
->valueLabel('Image URI')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
]),
|
||||
|
||||
Tab::make('Process Management')
|
||||
->columns()
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
Select::make('config_from')
|
||||
->label('Copy Settings From')
|
||||
@@ -130,6 +132,7 @@ class EditEgg extends EditRecord
|
||||
]),
|
||||
Tab::make('Egg Variables')
|
||||
->columnSpanFull()
|
||||
->icon('tabler-variable')
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
->label('')
|
||||
@@ -165,7 +168,7 @@ class EditEgg extends EditRecord
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->required(),
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
@@ -211,22 +214,19 @@ class EditEgg extends EditRecord
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
->columns(3)
|
||||
->icon('tabler-file-download')
|
||||
->schema([
|
||||
|
||||
Select::make('copy_script_from')
|
||||
->placeholder('None')
|
||||
->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||
|
||||
TextInput::make('script_container')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('alpine:3.4'),
|
||||
|
||||
TextInput::make('script_entry')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('ash'),
|
||||
|
||||
MonacoEditor::make('script_install')
|
||||
->label('Install Script')
|
||||
->columnSpanFull()
|
||||
@@ -234,7 +234,6 @@ class EditEgg extends EditRecord
|
||||
->language('shell')
|
||||
->view('filament.plugins.monaco-editor'),
|
||||
]),
|
||||
|
||||
])->columnSpanFull()->persistTabInQueryString(),
|
||||
]);
|
||||
}
|
||||
@@ -281,10 +280,7 @@ class EditEgg extends EditRecord
|
||||
->contained(false),
|
||||
|
||||
])
|
||||
->action(function (array $data, Egg $egg): void {
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
->action(function (array $data, Egg $egg, EggImporterService $eggImportService): void {
|
||||
if (!empty($data['egg'])) {
|
||||
try {
|
||||
$eggImportService->fromFile($data['egg'], $egg);
|
||||
|
||||
@@ -14,7 +14,7 @@ use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
@@ -49,7 +49,7 @@ class ListEggs extends ListRecords
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Tables\Actions\Action::make('export')
|
||||
Action::make('export')
|
||||
->icon('tabler-download')
|
||||
->label('Export')
|
||||
->color('primary')
|
||||
@@ -57,6 +57,39 @@ class ListEggs extends ListRecords
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->authorize(fn () => auth()->user()->can('export egg')),
|
||||
Action::make('update')
|
||||
->icon('tabler-cloud-download')
|
||||
->label('Update')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Are you sure you want to update this egg?')
|
||||
->modalDescription('If you made any changes to the egg they will be overwritten!')
|
||||
->modalIconColor('danger')
|
||||
->modalSubmitAction(fn (Actions\StaticAction $action) => $action->color('danger'))
|
||||
->action(function (Egg $egg, EggImporterService $eggImporterService) {
|
||||
try {
|
||||
$eggImporterService->fromUrl($egg->update_url, $egg);
|
||||
|
||||
cache()->forget("eggs.{$egg->uuid}.update");
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Egg Update failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Egg updated')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg'))
|
||||
->visible(fn (Egg $egg) => cache()->get("eggs.{$egg->uuid}.update", false)),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
@@ -65,6 +98,7 @@ class ListEggs extends ListRecords
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
@@ -97,10 +131,7 @@ class ListEggs extends ListRecords
|
||||
->contained(false),
|
||||
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
->action(function (array $data, EggImporterService $eggImportService): void {
|
||||
if (!empty($data['egg'])) {
|
||||
/** @var TemporaryUploadedFile[] $eggFile */
|
||||
$eggFile = $data['egg'];
|
||||
@@ -111,6 +142,7 @@ class ListEggs extends ListRecords
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@@ -127,6 +159,7 @@ class ListEggs extends ListRecords
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class ServersRelationManager extends RelationManager
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
|
||||
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned to this Egg.')
|
||||
->searchable(false)
|
||||
->columns([
|
||||
TextColumn::make('user.username')
|
||||
|
||||
@@ -11,6 +11,7 @@ class MountResource extends Resource
|
||||
protected static ?string $model = Mount::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -18,13 +19,6 @@ class MountResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -4,14 +4,14 @@ namespace App\Filament\Resources\MountResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MountResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditMount extends EditRecord
|
||||
{
|
||||
@@ -96,6 +96,7 @@ class EditMount extends EditRecord
|
||||
'lg' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -17,6 +17,7 @@ use Filament\Tables\Table;
|
||||
class ListMounts extends ListRecords
|
||||
{
|
||||
protected static string $resource = MountResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
@@ -56,6 +57,7 @@ class ListMounts extends ListRecords
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
@@ -153,7 +153,6 @@ class CreateNode extends CreateRecord
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
@@ -220,7 +219,7 @@ class CreateNode extends CreateRecord
|
||||
ToggleButtons::make('public')
|
||||
->default(true)
|
||||
->columnSpan(1)
|
||||
->label('Automatic Allocation')->inline()
|
||||
->label('Use Node for deployment?')->inline()
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
@@ -230,11 +229,7 @@ class CreateNode extends CreateRecord
|
||||
false => 'danger',
|
||||
]),
|
||||
TagsInput::make('tags')
|
||||
->label('Tags')
|
||||
->disabled()
|
||||
->placeholder('Not Implemented')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Not Implemented')
|
||||
->placeholder('Add Tags')
|
||||
->columnSpan(2),
|
||||
TextInput::make('upload_size')
|
||||
->label('Upload Limit')
|
||||
@@ -403,7 +398,7 @@ class CreateNode extends CreateRecord
|
||||
protected function getRedirectUrlParameters(): array
|
||||
{
|
||||
return [
|
||||
'tab' => '-configuration-tab',
|
||||
'tab' => '-configuration-file-tab',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,12 @@ namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use App\Services\Nodes\NodeAutoDeployService;
|
||||
use App\Services\Nodes\NodeUpdateService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions as FormActions;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
@@ -21,6 +24,7 @@ use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
@@ -52,7 +56,7 @@ class EditNode extends EditRecord
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->label('Wings Version')
|
||||
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
|
||||
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? 'Unknown') . ' (Latest: ' . $versionService->latestWingsVersion() . ')'),
|
||||
Placeholder::make('')
|
||||
->label('CPU Threads')
|
||||
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
|
||||
@@ -83,7 +87,7 @@ class EditNode extends EditRecord
|
||||
if (request()->isSecure()) {
|
||||
return '
|
||||
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
|
||||
You must use a domain name, because you cannot get SSL certificates for IP Addresses
|
||||
You must use a domain name, because you cannot get SSL certificates for IP Addresses.
|
||||
';
|
||||
}
|
||||
|
||||
@@ -98,7 +102,7 @@ class EditNode extends EditRecord
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return 'You cannot connect to an IP Address over SSL';
|
||||
return 'You cannot connect to an IP Address over SSL!';
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -130,11 +134,9 @@ class EditNode extends EditRecord
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
ToggleButtons::make('dns')
|
||||
->label('DNS Record Check')
|
||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||
@@ -151,20 +153,9 @@ class EditNode extends EditRecord
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
->columnSpan(1),
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->columnSpan(1)
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(1)
|
||||
@@ -172,7 +163,6 @@ class EditNode extends EditRecord
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
@@ -182,18 +172,11 @@ class EditNode extends EditRecord
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->columnSpan(1)
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
@@ -221,27 +204,48 @@ class EditNode extends EditRecord
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||
Tab::make('Advanced Settings')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
TextInput::make('id')
|
||||
->label('Node ID')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->disabled(),
|
||||
TextInput::make('uuid')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
])
|
||||
->label('Node UUID')
|
||||
->hintAction(CopyAction::make())
|
||||
->disabled(),
|
||||
TagsInput::make('tags')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->label('Tags')
|
||||
->disabled()
|
||||
->placeholder('Not Implemented')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Not Implemented'),
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
])
|
||||
->placeholder('Add Tags'),
|
||||
TextInput::make('upload_size')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Upload Limit')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
@@ -250,7 +254,12 @@ class EditNode extends EditRecord
|
||||
->maxValue(1024)
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||
TextInput::make('daemon_sftp')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 3,
|
||||
])
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
@@ -258,12 +267,22 @@ class EditNode extends EditRecord
|
||||
->required()
|
||||
->integer(),
|
||||
TextInput::make('daemon_sftp_alias')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 3,
|
||||
])
|
||||
->label('SFTP Alias')
|
||||
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
|
||||
ToggleButtons::make('public')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('Automatic Allocation')->inline()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 3,
|
||||
])
|
||||
->label('Use Node for deployment?')->inline()
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
@@ -273,7 +292,12 @@ class EditNode extends EditRecord
|
||||
false => 'danger',
|
||||
]),
|
||||
ToggleButtons::make('maintenance_mode')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 3,
|
||||
])
|
||||
->label('Maintenance Mode')->inline()
|
||||
->hinticon('tabler-question-mark')
|
||||
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||
@@ -286,7 +310,12 @@ class EditNode extends EditRecord
|
||||
true => 'danger',
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
@@ -303,14 +332,24 @@ class EditNode extends EditRecord
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->required()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
TextInput::make('memory_overallocate')
|
||||
@@ -320,14 +359,24 @@ class EditNode extends EditRecord
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
@@ -343,14 +392,24 @@ class EditNode extends EditRecord
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->required()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
TextInput::make('disk_overallocate')
|
||||
@@ -359,7 +418,12 @@ class EditNode extends EditRecord
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
@@ -422,19 +486,61 @@ class EditNode extends EditRecord
|
||||
->rows(19)
|
||||
->hintAction(CopyAction::make())
|
||||
->columnSpanFull(),
|
||||
Forms\Components\Actions::make([
|
||||
Forms\Components\Actions\Action::make('resetKey')
|
||||
->label('Reset Daemon Token')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Reset Daemon Token?')
|
||||
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
|
||||
->action(function (NodeUpdateService $nodeUpdateService, Node $node) {
|
||||
$nodeUpdateService->handle($node, [], true);
|
||||
Notification::make()->success()->title('Daemon Key Reset')->send();
|
||||
$this->fillForm();
|
||||
}),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns()
|
||||
->schema([
|
||||
FormActions::make([
|
||||
FormActions\Action::make('autoDeploy')
|
||||
->label('Auto Deploy Command')
|
||||
->color('primary')
|
||||
->modalHeading('Auto Deploy Command')
|
||||
->icon('tabler-rocket')
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelAction(false)
|
||||
->modalFooterActionsAlignment(Alignment::Center)
|
||||
->form([
|
||||
ToggleButtons::make('docker')
|
||||
->label('Type')
|
||||
->live()
|
||||
->helperText('Choose between Standalone and Docker install.')
|
||||
->inline()
|
||||
->default(false)
|
||||
->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state)))
|
||||
->options([
|
||||
false => 'Standalone',
|
||||
true => 'Docker',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'success',
|
||||
])
|
||||
->columnSpan(1),
|
||||
Textarea::make('generatedToken')
|
||||
->label('To auto-configure your node run the following command:')
|
||||
->readOnly()
|
||||
->autosize()
|
||||
->hintAction(fn (string $state) => CopyAction::make()->copyable($state))
|
||||
->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))),
|
||||
])
|
||||
->mountUsing(function (Forms\Form $form) {
|
||||
Notification::make()->success()->title('Autodeploy Generated')->send();
|
||||
$form->fill();
|
||||
}),
|
||||
])->fullWidth(),
|
||||
FormActions::make([
|
||||
FormActions\Action::make('resetKey')
|
||||
->label('Reset Daemon Token')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Reset Daemon Token?')
|
||||
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
|
||||
->action(function (NodeUpdateService $nodeUpdateService, Node $node) {
|
||||
$nodeUpdateService->handle($node, [], true);
|
||||
Notification::make()->success()->title('Daemon Key Reset')->send();
|
||||
$this->fillForm();
|
||||
}),
|
||||
])->fullWidth(),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
@@ -446,6 +552,16 @@ class EditNode extends EditRecord
|
||||
|
||||
$data['config'] = $node->getYamlConfiguration();
|
||||
|
||||
if (!is_ip($node->fqdn)) {
|
||||
$validRecords = gethostbynamel($node->fqdn);
|
||||
if ($validRecords) {
|
||||
$data['dns'] = true;
|
||||
$data['ip'] = collect($validRecords)->first();
|
||||
} else {
|
||||
$data['dns'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -453,6 +569,7 @@ class EditNode extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
@@ -468,11 +585,12 @@ class EditNode extends EditRecord
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
protected function getColumnSpan()
|
||||
protected function getColumnSpan(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
protected function getColumnStart()
|
||||
|
||||
protected function getColumnStart(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,12 @@ use App\Filament\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class ListNodes extends ListRecords
|
||||
{
|
||||
@@ -47,18 +46,18 @@ class ListNodes extends ListRecords
|
||||
->icon('tabler-device-desktop-analytics')
|
||||
->numeric()
|
||||
->suffix(config('panel.use_binary_prefix') ? ' GiB' : ' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), 2))
|
||||
->formatStateUsing(fn ($state) => Number::format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), maxPrecision: 2, locale: auth()->user()->language))
|
||||
->sortable(),
|
||||
TextColumn::make('disk')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-file')
|
||||
->numeric()
|
||||
->suffix(config('panel.use_binary_prefix') ? ' GiB' : ' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), 2))
|
||||
->formatStateUsing(fn ($state) => Number::format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), maxPrecision: 2, locale: auth()->user()->language))
|
||||
->sortable(),
|
||||
TextColumn::make('cpu')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-file')
|
||||
->icon('tabler-cpu')
|
||||
->numeric()
|
||||
->suffix(' %')
|
||||
->sortable(),
|
||||
@@ -82,12 +81,6 @@ class ListNodes extends ListRecords
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete node')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-server-2')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Nodes')
|
||||
|
||||
@@ -48,6 +48,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
// All assigned allocations
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||
->searchable()
|
||||
->selectCurrentPageOnly() //Prevent people from trying to nuke 30,000 ports at once.... -,-
|
||||
->columns([
|
||||
TextColumn::make('id'),
|
||||
TextColumn::make('port')
|
||||
@@ -65,12 +66,6 @@ class AllocationsRelationManager extends RelationManager
|
||||
->searchable()
|
||||
->label('IP'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
|
||||
->form(fn () => [
|
||||
@@ -148,7 +143,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord(), $data)),
|
||||
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
|
||||
class NodesRelationManager extends RelationManager
|
||||
{
|
||||
|
||||
@@ -7,10 +7,12 @@ use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class NodeCpuChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '5s';
|
||||
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
@@ -24,7 +26,7 @@ class NodeCpuChart extends ChartWidget
|
||||
$cpu = collect(cache()->get("nodes.$node->id.cpu_percent"))
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => number_format($value * $threads, 2),
|
||||
'cpu' => Number::format($value * $threads, maxPrecision: 2, locale: auth()->user()->language),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
@@ -73,8 +75,8 @@ class NodeCpuChart extends ChartWidget
|
||||
$node = $this->record;
|
||||
$threads = $node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$cpu = number_format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, 2);
|
||||
$max = number_format($threads * 100) . '%';
|
||||
$cpu = Number::format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, maxPrecision: 2, locale: auth()->user()->language);
|
||||
$max = Number::format($threads * 100, locale: auth()->user()->language) . '%';
|
||||
|
||||
return 'CPU - ' . $cpu . '% Of ' . $max;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class NodeMemoryChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '5s';
|
||||
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
@@ -22,7 +24,7 @@ class NodeMemoryChart extends ChartWidget
|
||||
|
||||
$memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'memory' => config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000,
|
||||
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
@@ -73,12 +75,12 @@ class NodeMemoryChart extends ChartWidget
|
||||
$totalMemory = collect(cache()->get("nodes.$node->id.memory_total"))->last();
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? number_format($latestMemoryUsed / 1024 / 1024 / 1024, 2) .' GiB'
|
||||
: number_format($latestMemoryUsed / 1000 / 1000 / 1000, 2) . ' GB';
|
||||
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? number_format($totalMemory / 1024 / 1024 / 1024, 2) .' GiB'
|
||||
: number_format($totalMemory / 1000 / 1000 / 1000, 2) . ' GB';
|
||||
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
return 'Memory - ' . $used . ' Of ' . $total;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class NodeStorageChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Storage';
|
||||
|
||||
protected static ?string $pollingInterval = '60s';
|
||||
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Enums\RolePermissionModels;
|
||||
use App\Enums\RolePermissionPrefixes;
|
||||
use App\Filament\Resources\RoleResource\Pages;
|
||||
use App\Models\Role;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Component;
|
||||
@@ -71,7 +70,7 @@ class RoleResource extends Resource
|
||||
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
TextInput::make('guard_name')
|
||||
->label('Guard Name')
|
||||
->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '')
|
||||
->default(Role::DEFAULT_GUARD_NAME)
|
||||
->nullable()
|
||||
->hidden(),
|
||||
Fieldset::make('Permissions')
|
||||
|
||||
@@ -19,13 +19,6 @@ class ServerResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -11,31 +11,58 @@ use App\Services\Allocations\AssignmentService;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use App\Services\Servers\ServerCreationService;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Wizard;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Closure;
|
||||
use LogicException;
|
||||
|
||||
class CreateServer extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ServerResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public ?Node $node = null;
|
||||
|
||||
private ServerCreationService $serverCreationService;
|
||||
|
||||
public function boot(ServerCreationService $serverCreationService): void
|
||||
{
|
||||
$this->serverCreationService = $serverCreationService;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Wizard::make([
|
||||
Wizard\Step::make('Information')
|
||||
Step::make('Information')
|
||||
->label('Information')
|
||||
->icon('tabler-info-circle')
|
||||
->completedIcon('tabler-check')
|
||||
@@ -46,12 +73,12 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->prefixIcon('tabler-server')
|
||||
->label('Name')
|
||||
->suffixAction(Forms\Components\Actions\Action::make('random')
|
||||
->icon('tabler-dice-' . random_int(1, 6))
|
||||
->action(function (Forms\Set $set, Forms\Get $get) {
|
||||
->action(function (Set $set, Get $get) {
|
||||
$egg = Egg::find($get('egg_id'));
|
||||
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
|
||||
|
||||
@@ -68,7 +95,7 @@ class CreateServer extends CreateRecord
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\Select::make('owner_id')
|
||||
Select::make('owner_id')
|
||||
->preload()
|
||||
->prefixIcon('tabler-user')
|
||||
->default(auth()->user()->id)
|
||||
@@ -83,29 +110,30 @@ class CreateServer extends CreateRecord
|
||||
->searchable(['username', 'email'])
|
||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->isRootAdmin() ? '(admin)' : ''))
|
||||
->createOptionForm([
|
||||
Forms\Components\TextInput::make('username')
|
||||
TextInput::make('username')
|
||||
->alphaNum()
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('email')
|
||||
TextInput::make('email')
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
TextInput::make('password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||
->password(),
|
||||
])
|
||||
->createOptionUsing(function ($data) {
|
||||
resolve(UserCreationService::class)->handle($data);
|
||||
->createOptionUsing(function ($data, UserCreationService $service) {
|
||||
$service->handle($data);
|
||||
|
||||
$this->refreshForm();
|
||||
})
|
||||
->required(),
|
||||
|
||||
Forms\Components\Select::make('node_id')
|
||||
Select::make('node_id')
|
||||
->disabledOn('edit')
|
||||
->prefixIcon('tabler-server-2')
|
||||
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
|
||||
@@ -119,13 +147,13 @@ class CreateServer extends CreateRecord
|
||||
->relationship('node', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->afterStateUpdated(function (Forms\Set $set, $state) {
|
||||
->afterStateUpdated(function (Set $set, $state) {
|
||||
$set('allocation_id', null);
|
||||
$this->node = Node::find($state);
|
||||
})
|
||||
->required(),
|
||||
|
||||
Forms\Components\Select::make('allocation_id')
|
||||
Select::make('allocation_id')
|
||||
->preload()
|
||||
->live()
|
||||
->prefixIcon('tabler-network')
|
||||
@@ -136,9 +164,9 @@ class CreateServer extends CreateRecord
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->disabled(fn (Forms\Get $get) => $get('node_id') === null)
|
||||
->disabled(fn (Get $get) => $get('node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->afterStateUpdated(function (Forms\Set $set) {
|
||||
->afterStateUpdated(function (Set $set) {
|
||||
$set('allocation_additional', null);
|
||||
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
|
||||
})
|
||||
@@ -146,7 +174,7 @@ class CreateServer extends CreateRecord
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->placeholder(function (Forms\Get $get) {
|
||||
->placeholder(function (Get $get) {
|
||||
$node = Node::find($get('node_id'));
|
||||
|
||||
if ($node?->allocations) {
|
||||
@@ -158,12 +186,12 @@ class CreateServer extends CreateRecord
|
||||
->relationship(
|
||||
'allocation',
|
||||
'ip',
|
||||
fn (Builder $query, Forms\Get $get) => $query
|
||||
fn (Builder $query, Get $get) => $query
|
||||
->where('node_id', $get('node_id'))
|
||||
->whereNull('server_id'),
|
||||
)
|
||||
->createOptionForm(fn (Forms\Get $get) => [
|
||||
Forms\Components\TextInput::make('allocation_ip')
|
||||
->createOptionForm(fn (Get $get) => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist(Node::find($get('node_id'))?->ipAddresses() ?? [])
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
@@ -171,7 +199,7 @@ class CreateServer extends CreateRecord
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
// ->selectablePlaceholder(false)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('allocation_alias')
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
@@ -181,7 +209,7 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
Forms\Components\TagsInput::make('allocation_ports')
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
@@ -191,7 +219,7 @@ class CreateServer extends CreateRecord
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
@@ -243,14 +271,14 @@ class CreateServer extends CreateRecord
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->createOptionUsing(function (array $data, Forms\Get $get): int {
|
||||
->createOptionUsing(function (array $data, Get $get, AssignmentService $assignmentService): int {
|
||||
return collect(
|
||||
resolve(AssignmentService::class)->handle(Node::find($get('node_id')), $data)
|
||||
$assignmentService->handle(Node::find($get('node_id')), $data)
|
||||
)->first();
|
||||
})
|
||||
->required(),
|
||||
|
||||
Forms\Components\Repeater::make('allocation_additional')
|
||||
Repeater::make('allocation_additional')
|
||||
->label('Additional Allocations')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
@@ -259,18 +287,18 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 3,
|
||||
])
|
||||
->addActionLabel('Add Allocation')
|
||||
->disabled(fn (Forms\Get $get) => $get('allocation_id') === null)
|
||||
->disabled(fn (Get $get) => $get('allocation_id') === null)
|
||||
// ->addable() TODO disable when all allocations are taken
|
||||
// ->addable() TODO disable until first additional allocation is selected
|
||||
->simple(
|
||||
Forms\Components\Select::make('extra_allocations')
|
||||
Select::make('extra_allocations')
|
||||
->live()
|
||||
->preload()
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->prefixIcon('tabler-network')
|
||||
->label('Additional Allocations')
|
||||
->columnSpan(2)
|
||||
->disabled(fn (Forms\Get $get) => $get('../../node_id') === null)
|
||||
->disabled(fn (Get $get) => $get('../../node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
@@ -281,14 +309,14 @@ class CreateServer extends CreateRecord
|
||||
->relationship(
|
||||
'allocations',
|
||||
'ip',
|
||||
fn (Builder $query, Forms\Get $get, Forms\Components\Select $component, $state) => $query
|
||||
fn (Builder $query, Get $get, Select $component, $state) => $query
|
||||
->where('node_id', $get('../../node_id'))
|
||||
->whereNot('id', $get('../../allocation_id'))
|
||||
->whereNull('server_id'),
|
||||
),
|
||||
),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
Textarea::make('description')
|
||||
->placeholder('Description')
|
||||
->rows(3)
|
||||
->columnSpan([
|
||||
@@ -297,10 +325,10 @@ class CreateServer extends CreateRecord
|
||||
'md' => 6,
|
||||
'lg' => 6,
|
||||
])
|
||||
->label('Notes'),
|
||||
->label('Description'),
|
||||
]),
|
||||
|
||||
Wizard\Step::make('Egg Configuration')
|
||||
Step::make('Egg Configuration')
|
||||
->label('Egg Configuration')
|
||||
->icon('tabler-egg')
|
||||
->completedIcon('tabler-check')
|
||||
@@ -311,7 +339,7 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Select::make('egg_id')
|
||||
Select::make('egg_id')
|
||||
->prefixIcon('tabler-egg')
|
||||
->relationship('egg', 'name')
|
||||
->columnSpan([
|
||||
@@ -323,7 +351,7 @@ class CreateServer extends CreateRecord
|
||||
->searchable()
|
||||
->preload()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Forms\Set $set, Forms\Get $get, $old) {
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get, $old) {
|
||||
$egg = Egg::query()->find($state);
|
||||
$set('startup', $egg->startup ?? '');
|
||||
$set('image', '');
|
||||
@@ -351,7 +379,7 @@ class CreateServer extends CreateRecord
|
||||
})
|
||||
->required(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||
ToggleButtons::make('skip_scripts')
|
||||
->label('Run Egg Install Script?')
|
||||
->default(false)
|
||||
->columnSpan([
|
||||
@@ -375,7 +403,7 @@ class CreateServer extends CreateRecord
|
||||
->inline()
|
||||
->required(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('start_on_completion')
|
||||
ToggleButtons::make('start_on_completion')
|
||||
->label('Start Server After Install?')
|
||||
->default(true)
|
||||
->required()
|
||||
@@ -399,10 +427,10 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->inline(),
|
||||
|
||||
Forms\Components\Textarea::make('startup')
|
||||
Textarea::make('startup')
|
||||
->hintIcon('tabler-code')
|
||||
->label('Startup Command')
|
||||
->hidden(fn (Forms\Get $get) => $get('egg_id') === null)
|
||||
->hidden(fn (Get $get) => $get('egg_id') === null)
|
||||
->required()
|
||||
->live()
|
||||
->rows(function ($state) {
|
||||
@@ -418,24 +446,24 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 6,
|
||||
]),
|
||||
|
||||
Forms\Components\Hidden::make('environment')->default([]),
|
||||
Hidden::make('environment')->default([]),
|
||||
|
||||
Forms\Components\Section::make('Variables')
|
||||
Section::make('Variables')
|
||||
->icon('tabler-eggs')
|
||||
->iconColor('primary')
|
||||
->hidden(fn (Forms\Get $get) => $get('egg_id') === null)
|
||||
->hidden(fn (Get $get) => $get('egg_id') === null)
|
||||
->collapsible()
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\Placeholder::make('Select an egg first to show its variables!')
|
||||
->hidden(fn (Forms\Get $get) => $get('egg_id')),
|
||||
Placeholder::make('Select an egg first to show its variables!')
|
||||
->hidden(fn (Get $get) => $get('egg_id')),
|
||||
|
||||
Forms\Components\Placeholder::make('The selected egg has no variables!')
|
||||
->hidden(fn (Forms\Get $get) => !$get('egg_id') ||
|
||||
Placeholder::make('The selected egg has no variables!')
|
||||
->hidden(fn (Get $get) => !$get('egg_id') ||
|
||||
Egg::query()->find($get('egg_id'))?->variables()?->count()
|
||||
),
|
||||
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
Repeater::make('server_variables')
|
||||
->label('')
|
||||
->relationship('serverVariables')
|
||||
->saveRelationshipsBeforeChildrenUsing(null)
|
||||
@@ -448,11 +476,11 @@ class CreateServer extends CreateRecord
|
||||
->hidden(fn ($state) => empty($state))
|
||||
->schema(function () {
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->required(fn (Forms\Get $get) => in_array('required', $get('rules')))
|
||||
->required(fn (Get $get) => in_array('required', $get('rules')))
|
||||
->rules(
|
||||
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $get('rules'),
|
||||
]);
|
||||
@@ -465,7 +493,7 @@ class CreateServer extends CreateRecord
|
||||
},
|
||||
);
|
||||
|
||||
$select = Forms\Components\Select::make('variable_value')
|
||||
$select = Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
@@ -476,11 +504,11 @@ class CreateServer extends CreateRecord
|
||||
$component = $component
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (Forms\Get $get) => $get('name'))
|
||||
->hintIconTooltip(fn (Forms\Get $get) => implode('|', $get('rules')))
|
||||
->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}')
|
||||
->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description'))
|
||||
->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
||||
->label(fn (Get $get) => $get('name'))
|
||||
->hintIconTooltip(fn (Get $get) => implode('|', $get('rules')))
|
||||
->prefix(fn (Get $get) => '{{' . $get('env_variable') . '}}')
|
||||
->helperText(fn (Get $get) => empty($get('description')) ? '—' : $get('description'))
|
||||
->afterStateUpdated(function (Set $set, Get $get, $state) {
|
||||
$environment = $get($envPath = '../../environment');
|
||||
$environment[$get('env_variable')] = $state;
|
||||
$set($envPath, $environment);
|
||||
@@ -492,12 +520,12 @@ class CreateServer extends CreateRecord
|
||||
->columnSpan(2),
|
||||
]),
|
||||
]),
|
||||
Wizard\Step::make('Environment Configuration')
|
||||
Step::make('Environment Configuration')
|
||||
->label('Environment Configuration')
|
||||
->icon('tabler-brand-docker')
|
||||
->completedIcon('tabler-check')
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make('Resource Limits')
|
||||
Fieldset::make('Resource Limits')
|
||||
->columnSpan(6)
|
||||
->columns([
|
||||
'default' => 1,
|
||||
@@ -506,14 +534,14 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->default(true)
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
@@ -525,9 +553,9 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('memory')
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->default(0)
|
||||
@@ -537,15 +565,15 @@ class CreateServer extends CreateRecord
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk Space')->inlineLabel()->inline()
|
||||
->default(true)
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
@@ -556,9 +584,9 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('disk')
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Space Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->default(0)
|
||||
@@ -568,14 +596,14 @@ class CreateServer extends CreateRecord
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_cpu')
|
||||
ToggleButtons::make('unlimited_cpu')
|
||||
->label('CPU')->inlineLabel()->inline()
|
||||
->default(true)
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
@@ -587,9 +615,9 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('cpu')
|
||||
TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('CPU Limit')->inlineLabel()
|
||||
->suffix('%')
|
||||
->default(0)
|
||||
@@ -599,24 +627,34 @@ class CreateServer extends CreateRecord
|
||||
->minValue(0)
|
||||
->helperText('100% equals one CPU core.'),
|
||||
]),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Fieldset::make('Advanced Limits')
|
||||
->columnSpan(6)
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('swap_support')
|
||||
ToggleButtons::make('swap_support')
|
||||
->live()
|
||||
->label('Enable Swap Memory')
|
||||
->label('Swap Memory')
|
||||
->inlineLabel()
|
||||
->inline()
|
||||
->columnSpan(2)
|
||||
->default('disabled')
|
||||
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$value = match ($state) {
|
||||
'unlimited' => -1,
|
||||
'disabled' => 0,
|
||||
'limited' => 128,
|
||||
default => throw new \LogicException('Invalid state'),
|
||||
default => throw new LogicException('Invalid state'),
|
||||
};
|
||||
|
||||
$set('swap', $value);
|
||||
@@ -632,9 +670,9 @@ class CreateServer extends CreateRecord
|
||||
'disabled' => 'danger',
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('swap')
|
||||
TextInput::make('swap')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
|
||||
->hidden(fn (Get $get) => match ($get('swap_support')) {
|
||||
'disabled', 'unlimited' => true,
|
||||
default => false,
|
||||
})
|
||||
@@ -648,16 +686,46 @@ class CreateServer extends CreateRecord
|
||||
->integer(),
|
||||
]),
|
||||
|
||||
Forms\Components\Hidden::make('io')
|
||||
Hidden::make('io')
|
||||
->helperText('The IO performance relative to other running containers')
|
||||
->label('Block IO Proportion')
|
||||
->default(500),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('oom_killer')
|
||||
ToggleButtons::make('cpu_pinning')
|
||||
->label('CPU Pinning')->inlineLabel()->inline()
|
||||
->default(false)
|
||||
->afterStateUpdated(fn (Set $set) => $set('threads', []))
|
||||
->live()
|
||||
->options([
|
||||
false => 'Disabled',
|
||||
true => 'Enabled',
|
||||
])
|
||||
->colors([
|
||||
false => 'success',
|
||||
true => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
TagsInput::make('threads')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => !$get('cpu_pinning'))
|
||||
->label('Pinned Threads')->inlineLabel()
|
||||
->required(fn (Get $get) => $get('cpu_pinning'))
|
||||
->columnSpan(2)
|
||||
->separator()
|
||||
->splitKeys([','])
|
||||
->placeholder('Add pinned thread, e.g. 0 or 2-4'),
|
||||
]),
|
||||
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('oom_killer')
|
||||
->label('OOM Killer')
|
||||
->inlineLabel()->inline()
|
||||
->default(false)
|
||||
@@ -671,12 +739,12 @@ class CreateServer extends CreateRecord
|
||||
true => 'danger',
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('oom_disabled_hidden')
|
||||
TextInput::make('oom_disabled_hidden')
|
||||
->hidden(),
|
||||
]),
|
||||
]),
|
||||
|
||||
Forms\Components\Fieldset::make('Feature Limits')
|
||||
Fieldset::make('Feature Limits')
|
||||
->inlineLabel()
|
||||
->columnSpan(6)
|
||||
->columns([
|
||||
@@ -686,21 +754,21 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('allocation_limit')
|
||||
TextInput::make('allocation_limit')
|
||||
->label('Allocations')
|
||||
->suffixIcon('tabler-network')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
Forms\Components\TextInput::make('database_limit')
|
||||
TextInput::make('database_limit')
|
||||
->label('Databases')
|
||||
->suffixIcon('tabler-database')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
Forms\Components\TextInput::make('backup_limit')
|
||||
TextInput::make('backup_limit')
|
||||
->label('Backups')
|
||||
->suffixIcon('tabler-copy-check')
|
||||
->required()
|
||||
@@ -708,7 +776,7 @@ class CreateServer extends CreateRecord
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
]),
|
||||
Forms\Components\Fieldset::make('Docker Settings')
|
||||
Fieldset::make('Docker Settings')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -717,10 +785,11 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->columnSpan(6)
|
||||
->schema([
|
||||
Forms\Components\Select::make('select_image')
|
||||
Select::make('select_image')
|
||||
->label('Image Name')
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
|
||||
->options(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
|
||||
->options(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
@@ -741,10 +810,10 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 2,
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('image')
|
||||
TextInput::make('image')
|
||||
->label('Image')
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||
->required()
|
||||
->afterStateUpdated(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
@@ -762,13 +831,13 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 2,
|
||||
]),
|
||||
|
||||
Forms\Components\KeyValue::make('docker_labels')
|
||||
KeyValue::make('docker_labels')
|
||||
->label('Container Labels')
|
||||
->keyLabel('Title')
|
||||
->valueLabel('Description')
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\CheckboxList::make('mounts')
|
||||
CheckboxList::make('mounts')
|
||||
->live()
|
||||
->relationship('mounts')
|
||||
->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
|
||||
@@ -806,30 +875,27 @@ class CreateServer extends CreateRecord
|
||||
{
|
||||
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
|
||||
|
||||
/** @var ServerCreationService $service */
|
||||
$service = resolve(ServerCreationService::class);
|
||||
|
||||
return $service->handle($data);
|
||||
return $this->serverCreationService->handle($data);
|
||||
}
|
||||
|
||||
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||
private function shouldHideComponent(Get $get, Component $component): bool
|
||||
{
|
||||
$containsRuleIn = collect($get('rules'))->reduce(
|
||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||
);
|
||||
|
||||
if ($component instanceof Forms\Components\Select) {
|
||||
if ($component instanceof Select) {
|
||||
return $containsRuleIn;
|
||||
}
|
||||
|
||||
if ($component instanceof Forms\Components\TextInput) {
|
||||
if ($component instanceof TextInput) {
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
|
||||
throw new \Exception('Component type not supported: ' . $component::class);
|
||||
throw new Exception('Component type not supported: ' . $component::class);
|
||||
}
|
||||
|
||||
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||
private function getSelectOptionsFromRules(Get $get): array
|
||||
{
|
||||
$inRule = collect($get('rules'))->reduce(
|
||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||
|
||||
@@ -2,34 +2,49 @@
|
||||
|
||||
namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Models\Database;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use LogicException;
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Http\Controllers\Admin\ServersController;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Filament\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
|
||||
use App\Models\Database;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use App\Services\Servers\ServerDeletionService;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\ToggleInstallService;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use LogicException;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class EditServer extends EditRecord
|
||||
@@ -50,15 +65,15 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Information')
|
||||
Tab::make('Information')
|
||||
->icon('tabler-info-circle')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
TextInput::make('name')
|
||||
->prefixIcon('tabler-server')
|
||||
->label('Display Name')
|
||||
->suffixAction(Forms\Components\Actions\Action::make('random')
|
||||
->suffixAction(Action::make('random')
|
||||
->icon('tabler-dice-' . random_int(1, 6))
|
||||
->action(function (Forms\Set $set, Forms\Get $get) {
|
||||
->action(function (Set $set, Get $get) {
|
||||
$egg = Egg::find($get('egg_id'));
|
||||
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
|
||||
|
||||
@@ -75,7 +90,7 @@ class EditServer extends EditRecord
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\Select::make('owner_id')
|
||||
Select::make('owner_id')
|
||||
->prefixIcon('tabler-user')
|
||||
->label('Owner')
|
||||
->columnSpan([
|
||||
@@ -89,7 +104,7 @@ class EditServer extends EditRecord
|
||||
->preload()
|
||||
->required(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('condition')
|
||||
ToggleButtons::make('condition')
|
||||
->label('Server Status')
|
||||
->formatStateUsing(fn (Server $server) => $server->condition)
|
||||
->options(fn ($state) => collect(array_merge(ContainerStatus::cases(), ServerState::cases()))
|
||||
@@ -109,11 +124,11 @@ class EditServer extends EditRecord
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
Textarea::make('description')
|
||||
->label('Description')
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\TextInput::make('uuid')
|
||||
TextInput::make('uuid')
|
||||
->hintAction(CopyAction::make())
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
@@ -123,7 +138,7 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->readOnly()
|
||||
->dehydrated(false),
|
||||
Forms\Components\TextInput::make('uuid_short')
|
||||
TextInput::make('uuid_short')
|
||||
->label('Short UUID')
|
||||
->hintAction(CopyAction::make())
|
||||
->columnSpan([
|
||||
@@ -134,7 +149,7 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->readOnly()
|
||||
->dehydrated(false),
|
||||
Forms\Components\TextInput::make('external_id')
|
||||
TextInput::make('external_id')
|
||||
->label('External ID')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
@@ -143,7 +158,7 @@ class EditServer extends EditRecord
|
||||
'lg' => 3,
|
||||
])
|
||||
->maxLength(255),
|
||||
Forms\Components\Select::make('node_id')
|
||||
Select::make('node_id')
|
||||
->label('Node')
|
||||
->relationship('node', 'name')
|
||||
->columnSpan([
|
||||
@@ -154,10 +169,10 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->disabled(),
|
||||
]),
|
||||
Tabs\Tab::make('Environment')
|
||||
Tab::make('Environment')
|
||||
->icon('tabler-brand-docker')
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make('Resource Limits')
|
||||
Fieldset::make('Resource Limits')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -165,14 +180,14 @@ class EditServer extends EditRecord
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
@@ -184,9 +199,9 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('memory')
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->required()
|
||||
@@ -195,15 +210,15 @@ class EditServer extends EditRecord
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk Space')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
@@ -214,9 +229,9 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('disk')
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Space Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->required()
|
||||
@@ -225,14 +240,14 @@ class EditServer extends EditRecord
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('unlimited_cpu')
|
||||
ToggleButtons::make('unlimited_cpu')
|
||||
->label('CPU')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
|
||||
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
@@ -244,9 +259,9 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\TextInput::make('cpu')
|
||||
TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('CPU Limit')->inlineLabel()
|
||||
->suffix('%')
|
||||
->required()
|
||||
@@ -254,16 +269,25 @@ class EditServer extends EditRecord
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
]),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Fieldset::make('Advanced Limits')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('swap_support')
|
||||
ToggleButtons::make('swap_support')
|
||||
->live()
|
||||
->label('Enable Swap Memory')->inlineLabel()->inline()
|
||||
->label('Swap Memory')->inlineLabel()->inline()
|
||||
->columnSpan(2)
|
||||
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$value = match ($state) {
|
||||
'unlimited' => -1,
|
||||
'disabled' => 0,
|
||||
@@ -273,7 +297,7 @@ class EditServer extends EditRecord
|
||||
|
||||
$set('swap', $value);
|
||||
})
|
||||
->formatStateUsing(function (Forms\Get $get) {
|
||||
->formatStateUsing(function (Get $get) {
|
||||
return match (true) {
|
||||
$get('swap') > 0 => 'limited',
|
||||
$get('swap') == 0 => 'disabled',
|
||||
@@ -292,9 +316,9 @@ class EditServer extends EditRecord
|
||||
'disabled' => 'danger',
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('swap')
|
||||
TextInput::make('swap')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
|
||||
->hidden(fn (Get $get) => match ($get('swap_support')) {
|
||||
'disabled', 'unlimited', true => true,
|
||||
default => false,
|
||||
})
|
||||
@@ -306,15 +330,46 @@ class EditServer extends EditRecord
|
||||
->integer(),
|
||||
]),
|
||||
|
||||
Forms\Components\Hidden::make('io')
|
||||
Hidden::make('io')
|
||||
->helperText('The IO performance relative to other running containers')
|
||||
->label('Block IO Proportion'),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('oom_killer')
|
||||
ToggleButtons::make('cpu_pinning')
|
||||
->label('CPU Pinning')->inlineLabel()->inline()
|
||||
->default(false)
|
||||
->afterStateUpdated(fn (Set $set) => $set('threads', []))
|
||||
->formatStateUsing(fn (Get $get) => !empty($get('threads')))
|
||||
->live()
|
||||
->options([
|
||||
false => 'Disabled',
|
||||
true => 'Enabled',
|
||||
])
|
||||
->colors([
|
||||
false => 'success',
|
||||
true => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
TagsInput::make('threads')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => !$get('cpu_pinning'))
|
||||
->label('Pinned Threads')->inlineLabel()
|
||||
->required(fn (Get $get) => $get('cpu_pinning'))
|
||||
->columnSpan(2)
|
||||
->separator()
|
||||
->splitKeys([','])
|
||||
->placeholder('Add pinned thread, e.g. 0 or 2-4'),
|
||||
]),
|
||||
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('oom_killer')
|
||||
->label('OOM Killer')->inlineLabel()->inline()
|
||||
->columnSpan(2)
|
||||
->options([
|
||||
@@ -326,12 +381,12 @@ class EditServer extends EditRecord
|
||||
true => 'danger',
|
||||
]),
|
||||
|
||||
Forms\Components\TextInput::make('oom_disabled_hidden')
|
||||
TextInput::make('oom_disabled_hidden')
|
||||
->hidden(),
|
||||
]),
|
||||
]),
|
||||
|
||||
Forms\Components\Fieldset::make('Feature Limits')
|
||||
Fieldset::make('Feature Limits')
|
||||
->inlineLabel()
|
||||
->columns([
|
||||
'default' => 1,
|
||||
@@ -340,23 +395,23 @@ class EditServer extends EditRecord
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('allocation_limit')
|
||||
TextInput::make('allocation_limit')
|
||||
->suffixIcon('tabler-network')
|
||||
->required()
|
||||
->minValue(0)
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('database_limit')
|
||||
TextInput::make('database_limit')
|
||||
->suffixIcon('tabler-database')
|
||||
->required()
|
||||
->minValue(0)
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('backup_limit')
|
||||
TextInput::make('backup_limit')
|
||||
->suffixIcon('tabler-copy-check')
|
||||
->required()
|
||||
->minValue(0)
|
||||
->numeric(),
|
||||
]),
|
||||
Forms\Components\Fieldset::make('Docker Settings')
|
||||
Fieldset::make('Docker Settings')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -364,10 +419,11 @@ class EditServer extends EditRecord
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Select::make('select_image')
|
||||
Select::make('select_image')
|
||||
->label('Image Name')
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
|
||||
->options(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
|
||||
->options(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
@@ -383,10 +439,10 @@ class EditServer extends EditRecord
|
||||
->selectablePlaceholder(false)
|
||||
->columnSpan(1),
|
||||
|
||||
Forms\Components\TextInput::make('image')
|
||||
TextInput::make('image')
|
||||
->label('Image')
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||
->required()
|
||||
->afterStateUpdated(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
@@ -399,14 +455,14 @@ class EditServer extends EditRecord
|
||||
->placeholder('Enter a custom Image')
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\KeyValue::make('docker_labels')
|
||||
KeyValue::make('docker_labels')
|
||||
->label('Container Labels')
|
||||
->keyLabel('Label Name')
|
||||
->valueLabel('Label Description')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Egg')
|
||||
Tab::make('Egg')
|
||||
->icon('tabler-egg')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
@@ -415,7 +471,7 @@ class EditServer extends EditRecord
|
||||
'lg' => 5,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Select::make('egg_id')
|
||||
Select::make('egg_id')
|
||||
->disabledOn('edit')
|
||||
->prefixIcon('tabler-egg')
|
||||
->columnSpan([
|
||||
@@ -429,7 +485,7 @@ class EditServer extends EditRecord
|
||||
->preload()
|
||||
->required(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||
ToggleButtons::make('skip_scripts')
|
||||
->label('Run Egg Install Script?')->inline()
|
||||
->columnSpan([
|
||||
'default' => 6,
|
||||
@@ -451,7 +507,7 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->required(),
|
||||
|
||||
Forms\Components\Textarea::make('startup')
|
||||
Textarea::make('startup')
|
||||
->label('Startup Command')
|
||||
->required()
|
||||
->columnSpan(6)
|
||||
@@ -462,18 +518,18 @@ class EditServer extends EditRecord
|
||||
);
|
||||
}),
|
||||
|
||||
Forms\Components\Textarea::make('defaultStartup')
|
||||
Textarea::make('defaultStartup')
|
||||
->hintAction(CopyAction::make())
|
||||
->label('Default Startup Command')
|
||||
->disabled()
|
||||
->formatStateUsing(function ($state, Get $get, Set $set) {
|
||||
->formatStateUsing(function ($state, Get $get) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
|
||||
return $egg->startup;
|
||||
})
|
||||
->columnSpan(6),
|
||||
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
Repeater::make('server_variables')
|
||||
->relationship('serverVariables', function (Builder $query) {
|
||||
/** @var Server $server */
|
||||
$server = $this->getRecord();
|
||||
@@ -502,7 +558,7 @@ class EditServer extends EditRecord
|
||||
->reorderable(false)->addable(false)->deletable(false)
|
||||
->schema(function () {
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
|
||||
->rules([
|
||||
@@ -519,7 +575,7 @@ class EditServer extends EditRecord
|
||||
},
|
||||
]);
|
||||
|
||||
$select = Forms\Components\Select::make('variable_value')
|
||||
$select = Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
@@ -540,18 +596,18 @@ class EditServer extends EditRecord
|
||||
})
|
||||
->columnSpan(6),
|
||||
]),
|
||||
Tabs\Tab::make('Mounts')
|
||||
Tab::make('Mounts')
|
||||
->icon('tabler-layers-linked')
|
||||
->schema([
|
||||
Forms\Components\CheckboxList::make('mounts')
|
||||
CheckboxList::make('mounts')
|
||||
->relationship('mounts')
|
||||
->options(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
|
||||
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
|
||||
->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
|
||||
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
|
||||
->label('Mounts')
|
||||
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Tabs\Tab::make('Databases')
|
||||
Tab::make('Databases')
|
||||
->icon('tabler-database')
|
||||
->schema([
|
||||
Repeater::make('databases')
|
||||
@@ -559,7 +615,7 @@ class EditServer extends EditRecord
|
||||
->helperText(fn (Server $server) => $server->databases->isNotEmpty() ? '' : 'No Databases exist for this Server')
|
||||
->columns(2)
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('database')
|
||||
TextInput::make('database')
|
||||
->columnSpan(2)
|
||||
->label('Database Name')
|
||||
->disabled()
|
||||
@@ -570,11 +626,11 @@ class EditServer extends EditRecord
|
||||
->icon('tabler-trash')
|
||||
->action(fn (DatabaseManagementService $databaseManagementService, $record) => $databaseManagementService->delete($record))
|
||||
),
|
||||
Forms\Components\TextInput::make('username')
|
||||
TextInput::make('username')
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($record) => $record->username)
|
||||
->columnSpan(2),
|
||||
Forms\Components\TextInput::make('password')
|
||||
TextInput::make('password')
|
||||
->disabled()
|
||||
->hintAction(
|
||||
Action::make('rotate')
|
||||
@@ -584,30 +640,30 @@ class EditServer extends EditRecord
|
||||
)
|
||||
->formatStateUsing(fn (Database $database) => $database->password)
|
||||
->columnSpan(2),
|
||||
Forms\Components\TextInput::make('remote')
|
||||
TextInput::make('remote')
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($record) => $record->remote)
|
||||
->columnSpan(1)
|
||||
->label('Connections From'),
|
||||
Forms\Components\TextInput::make('max_connections')
|
||||
TextInput::make('max_connections')
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($record) => $record->max_connections)
|
||||
->columnSpan(1),
|
||||
Forms\Components\TextInput::make('JDBC')
|
||||
TextInput::make('JDBC')
|
||||
->disabled()
|
||||
->label('JDBC Connection String')
|
||||
->columnSpan(2)
|
||||
->formatStateUsing(fn (Forms\Get $get, $record) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($record->password) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database')),
|
||||
->formatStateUsing(fn (Get $get, $record) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($record->password) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database')),
|
||||
])
|
||||
->relationship('databases')
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->columnSpan(4),
|
||||
])->columns(4),
|
||||
Tabs\Tab::make('Actions')
|
||||
Tab::make('Actions')
|
||||
->icon('tabler-settings')
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make('Server Actions')
|
||||
Fieldset::make('Server Actions')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -615,27 +671,27 @@ class EditServer extends EditRecord
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
Forms\Components\Actions::make([
|
||||
Forms\Components\Actions\Action::make('toggleInstall')
|
||||
Action::make('toggleInstall')
|
||||
->label('Toggle Install Status')
|
||||
->disabled(fn (Server $server) => $server->isSuspended())
|
||||
->action(function (ServersController $serversController, Server $server) {
|
||||
$serversController->toggleInstall($server);
|
||||
->action(function (ToggleInstallService $service, Server $server) {
|
||||
$service->handle($server);
|
||||
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
}),
|
||||
])->fullWidth(),
|
||||
Forms\Components\ToggleButtons::make('')
|
||||
ToggleButtons::make('')
|
||||
->hint('If you need to change the install status from uninstalled to installed, or vice versa, you may do so with this button.'),
|
||||
]),
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
Forms\Components\Actions::make([
|
||||
Forms\Components\Actions\Action::make('toggleSuspend')
|
||||
Action::make('toggleSuspend')
|
||||
->label('Suspend')
|
||||
->color('warning')
|
||||
->hidden(fn (Server $server) => $server->isSuspended())
|
||||
@@ -645,7 +701,7 @@ class EditServer extends EditRecord
|
||||
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
}),
|
||||
Forms\Components\Actions\Action::make('toggleUnsuspend')
|
||||
Action::make('toggleUnsuspend')
|
||||
->label('Unsuspend')
|
||||
->color('success')
|
||||
->hidden(fn (Server $server) => !$server->isSuspended())
|
||||
@@ -656,37 +712,37 @@ class EditServer extends EditRecord
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
}),
|
||||
])->fullWidth(),
|
||||
Forms\Components\ToggleButtons::make('')
|
||||
ToggleButtons::make('')
|
||||
->hidden(fn (Server $server) => $server->isSuspended())
|
||||
->hint('This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API.'),
|
||||
Forms\Components\ToggleButtons::make('')
|
||||
ToggleButtons::make('')
|
||||
->hidden(fn (Server $server) => !$server->isSuspended())
|
||||
->hint('This will unsuspend the server and restore normal user access.'),
|
||||
]),
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
Forms\Components\Actions::make([
|
||||
Forms\Components\Actions\Action::make('transfer')
|
||||
Action::make('transfer')
|
||||
->label('Transfer Soon™')
|
||||
->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
|
||||
->disabled() //TODO!
|
||||
->form([ //TODO!
|
||||
Forms\Components\Select::make('newNode')
|
||||
Select::make('newNode')
|
||||
->label('New Node')
|
||||
->required()
|
||||
->options([
|
||||
true => 'on',
|
||||
false => 'off',
|
||||
]),
|
||||
Forms\Components\Select::make('newMainAllocation')
|
||||
Select::make('newMainAllocation')
|
||||
->label('New Main Allocation')
|
||||
->required()
|
||||
->options([
|
||||
true => 'on',
|
||||
false => 'off',
|
||||
]),
|
||||
Forms\Components\Select::make('newAdditionalAllocation')
|
||||
Select::make('newAdditionalAllocation')
|
||||
->label('New Additional Allocations')
|
||||
->options([
|
||||
true => 'on',
|
||||
@@ -695,23 +751,23 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->modalHeading('Transfer'),
|
||||
])->fullWidth(),
|
||||
Forms\Components\ToggleButtons::make('')
|
||||
ToggleButtons::make('')
|
||||
->hint('Transfer this server to another node connected to this panel. Warning! This feature has not been fully tested and may have bugs.'),
|
||||
]),
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
Forms\Components\Actions::make([
|
||||
Forms\Components\Actions\Action::make('reinstall')
|
||||
Action::make('reinstall')
|
||||
->label('Reinstall')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Are you sure you want to reinstall this server?')
|
||||
->modalDescription('!! This can result in unrecoverable data loss !!')
|
||||
->disabled(fn (Server $server) => $server->isSuspended())
|
||||
->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)),
|
||||
->action(fn (ReinstallServerService $service, Server $server) => $service->handle($server)),
|
||||
])->fullWidth(),
|
||||
Forms\Components\ToggleButtons::make('')
|
||||
ToggleButtons::make('')
|
||||
->hint('This will reinstall the server with the assigned egg install script.'),
|
||||
]),
|
||||
]),
|
||||
@@ -725,13 +781,14 @@ class EditServer extends EditRecord
|
||||
return $form
|
||||
->columns()
|
||||
->schema([
|
||||
Forms\Components\Select::make('toNode')
|
||||
Select::make('toNode')
|
||||
->label('New Node'),
|
||||
Forms\Components\TextInput::make('newAllocation')
|
||||
TextInput::make('newAllocation')
|
||||
->label('Allocation'),
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
@@ -740,11 +797,12 @@ class EditServer extends EditRecord
|
||||
->color('danger')
|
||||
->label('Delete')
|
||||
->requiresConfirmation()
|
||||
->action(function (Server $server) {
|
||||
resolve(ServerDeletionService::class)->handle($server);
|
||||
->action(function (Server $server, ServerDeletionService $service) {
|
||||
$service->handle($server);
|
||||
|
||||
return redirect(ListServers::getUrl());
|
||||
}),
|
||||
})
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
Actions\Action::make('console')
|
||||
->label('Console')
|
||||
->icon('tabler-terminal')
|
||||
@@ -753,6 +811,7 @@ class EditServer extends EditRecord
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
@@ -772,7 +831,7 @@ class EditServer extends EditRecord
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
||||
AllocationsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -780,15 +839,15 @@ class EditServer extends EditRecord
|
||||
{
|
||||
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
|
||||
|
||||
if ($component instanceof Forms\Components\Select) {
|
||||
if ($component instanceof Select) {
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
|
||||
if ($component instanceof Forms\Components\TextInput) {
|
||||
if ($component instanceof TextInput) {
|
||||
return $containsRuleIn;
|
||||
}
|
||||
|
||||
throw new \Exception('Component type not supported: ' . $component::class);
|
||||
throw new Exception('Component type not supported: ' . $component::class);
|
||||
}
|
||||
|
||||
private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
|
||||
@@ -803,7 +862,7 @@ class EditServer extends EditRecord
|
||||
->all();
|
||||
}
|
||||
|
||||
protected function rotatePassword(DatabasePasswordService $service, $record, $set, $get): void
|
||||
protected function rotatePassword(DatabasePasswordService $service, Database $record, Set $set, Get $get): void
|
||||
{
|
||||
$newPassword = $service->handle($record);
|
||||
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database');
|
||||
|
||||
@@ -4,13 +4,15 @@ namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Grouping\Group;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListServers extends ListRecords
|
||||
{
|
||||
@@ -27,47 +29,50 @@ class ListServers extends ListRecords
|
||||
Group::make('egg.name')->getDescriptionFromRecordUsing(fn (Server $server): string => str($server->egg->description)->limit(150)),
|
||||
])
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('condition')
|
||||
TextColumn::make('condition')
|
||||
->default('unknown')
|
||||
->badge()
|
||||
->icon(fn (Server $server) => $server->conditionIcon())
|
||||
->color(fn (Server $server) => $server->conditionColor()),
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
TextColumn::make('uuid')
|
||||
->hidden()
|
||||
->label('UUID')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->icon('tabler-brand-docker')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('node.name')
|
||||
TextColumn::make('node.name')
|
||||
->icon('tabler-server-2')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
|
||||
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'node.name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('egg.name')
|
||||
TextColumn::make('egg.name')
|
||||
->icon('tabler-egg')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
|
||||
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'egg.name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('user.username')
|
||||
TextColumn::make('user.username')
|
||||
->icon('tabler-user')
|
||||
->label('Owner')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'user.username')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\SelectColumn::make('allocation_id')
|
||||
SelectColumn::make('allocation_id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(
|
||||
fn ($allocation) => [$allocation->id => $allocation->address])
|
||||
)
|
||||
->hidden(!auth()->user()->can('update server'))
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('image')->hidden(),
|
||||
Tables\Columns\TextColumn::make('backups_count')
|
||||
TextColumn::make('allocation_id_readonly')
|
||||
->label('Primary Allocation')
|
||||
->hidden(auth()->user()->can('update server'))
|
||||
->state(fn (Server $server) => $server->allocation->address),
|
||||
TextColumn::make('image')->hidden(),
|
||||
TextColumn::make('backups_count')
|
||||
->counts('backups')
|
||||
->label('Backups')
|
||||
->icon('tabler-file-download')
|
||||
@@ -75,16 +80,11 @@ class ListServers extends ListRecords
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\Action::make('View')
|
||||
Action::make('View')
|
||||
->icon('tabler-terminal')
|
||||
->url(fn (Server $server) => "/server/$server->uuid_short")
|
||||
->visible(function (Server $server) {
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user->isRootAdmin() || $user->id === $server->owner_id;
|
||||
}),
|
||||
Tables\Actions\EditAction::make(),
|
||||
->authorize(fn () => auth()->user()->can('view server')),
|
||||
EditAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
->searchable()
|
||||
@@ -96,6 +96,7 @@ class ListServers extends ListRecords
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -7,10 +7,16 @@ use App\Models\Server;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\AssociateAction;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
@@ -34,17 +40,16 @@ class AllocationsRelationManager extends RelationManager
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->selectCurrentPageOnly()
|
||||
->recordTitleAttribute('ip')
|
||||
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
|
||||
// ->actions
|
||||
// ->groups
|
||||
->inverseRelationship('server')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('ip')->label('IP'),
|
||||
Tables\Columns\TextColumn::make('port')->label('Port'),
|
||||
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
|
||||
Tables\Columns\IconColumn::make('primary')
|
||||
TextColumn::make('ip')->label('IP'),
|
||||
TextColumn::make('port')->label('Port'),
|
||||
TextInputColumn::make('ip_alias')->label('Alias'),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
@@ -53,20 +58,17 @@ class AllocationsRelationManager extends RelationManager
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
|
||||
->label('Primary'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\Action::make('make-primary')
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
|
||||
Action::make('make-primary')
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()->label('Create Allocation')
|
||||
CreateAction::make()->label('Create Allocation')
|
||||
->createAnother(false)
|
||||
->form(fn () => [
|
||||
TextInput::make('allocation_ip')
|
||||
@@ -143,12 +145,12 @@ class AllocationsRelationManager extends RelationManager
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
|
||||
Tables\Actions\AssociateAction::make()
|
||||
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
|
||||
AssociateAction::make()
|
||||
->multiple()
|
||||
->associateAnother(false)
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
|
||||
->label('Add Allocation'),
|
||||
])
|
||||
->bulkActions([
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user