mirror of
https://github.com/pelican-dev/panel.git
synced 2026-02-24 03:12:01 +03:00
Compare commits
164 Commits
v1.0.0-bet
...
issue/68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eeefab6c6 | ||
|
|
a2108c3d91 | ||
|
|
4f5e9a6c30 | ||
|
|
bcbacb47cd | ||
|
|
e9f6fbadd4 | ||
|
|
c621d2dad5 | ||
|
|
64943aa50c | ||
|
|
020e41cbbc | ||
|
|
e162374e15 | ||
|
|
81c75f7966 | ||
|
|
2be8168468 | ||
|
|
465a372000 | ||
|
|
f0c536c045 | ||
|
|
6a8e630444 | ||
|
|
71aed151d9 | ||
|
|
bb5955cff4 | ||
|
|
38be89a71e | ||
|
|
deb6603840 | ||
|
|
c7a307af6e | ||
|
|
8740f0f645 | ||
|
|
466f9f7edc | ||
|
|
d21740d458 | ||
|
|
1bf6a880fb | ||
|
|
96acd268be | ||
|
|
c0a41acf1f | ||
|
|
75e89b2d4c | ||
|
|
54ea55d426 | ||
|
|
1aa51d15f0 | ||
|
|
3d8847a508 | ||
|
|
4fa49ae915 | ||
|
|
a42cb193a3 | ||
|
|
50ffaf4d01 | ||
|
|
804ff64a71 | ||
|
|
c2f6842f64 | ||
|
|
455d0543f1 | ||
|
|
97a4601150 | ||
|
|
2cc4a42905 | ||
|
|
5353d38302 | ||
|
|
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 | ||
|
|
df88d33af4 | ||
|
|
906d4a7d28 | ||
|
|
9ba8c1df9b | ||
|
|
0a6b846230 | ||
|
|
aff9f4ea37 | ||
|
|
f2754c3cb1 | ||
|
|
8b86707150 | ||
|
|
233fd50b2b | ||
|
|
3a76fb1c79 | ||
|
|
e76630b7f3 | ||
|
|
0f798e5edb | ||
|
|
a9c7eeddde | ||
|
|
11feef4f8c | ||
|
|
bebc410eda | ||
|
|
ec0fa3c913 | ||
|
|
4574821ed8 | ||
|
|
d71b1a4710 | ||
|
|
d9922e86f2 | ||
|
|
9d9e4adbbd | ||
|
|
6b104e3331 | ||
|
|
f2eca17480 | ||
|
|
4c41e659b5 | ||
|
|
6238d6dd08 | ||
|
|
c45e4edcf6 | ||
|
|
4273880126 | ||
|
|
d86843977b | ||
|
|
5b468c21ae | ||
|
|
68ef0a1d0a | ||
|
|
f6122f919a | ||
|
|
4f10ec2c20 | ||
|
|
45fcc2a09a | ||
|
|
19c7b4d044 | ||
|
|
7c8b204d13 | ||
|
|
343a5b81bc | ||
|
|
755632f9d5 | ||
|
|
e7ee86a914 | ||
|
|
eb0bad82e6 | ||
|
|
48fd3cc84e | ||
|
|
d4484f5254 | ||
|
|
958e8fac8a | ||
|
|
7986505b99 | ||
|
|
ba5b81cf2d | ||
|
|
32018399b6 | ||
|
|
48f4c35d0b | ||
|
|
f699fd5459 | ||
|
|
05573f64dd | ||
|
|
3fa714c7e3 | ||
|
|
69acc48b5e | ||
|
|
9d9720a5a2 | ||
|
|
ff261f9c99 | ||
|
|
c7bea4f024 | ||
|
|
738707b251 | ||
|
|
a8699704de | ||
|
|
d9dc932e07 | ||
|
|
f57232bc23 | ||
|
|
b24ff8bb26 | ||
|
|
eff8e509ef | ||
|
|
6976fa8989 | ||
|
|
2b58160da9 | ||
|
|
44e0dd3e09 | ||
|
|
8ea57bc46b | ||
|
|
459d90e8d1 | ||
|
|
a97341f6f2 | ||
|
|
375a64a38e | ||
|
|
7c25fc2a9d | ||
|
|
1a26f5ce9e | ||
|
|
b47f40bd13 | ||
|
|
bcb7240ed2 | ||
|
|
405aa857b1 | ||
|
|
0bd2935885 | ||
|
|
4cba1540ac | ||
|
|
30051ab0d7 | ||
|
|
e15d515f71 | ||
|
|
36e2fa8e2b | ||
|
|
510ae3c0df | ||
|
|
f5edb34873 | ||
|
|
0895bd2be5 | ||
|
|
17bc3de0d0 | ||
|
|
4319f24f51 | ||
|
|
9ad113bc61 | ||
|
|
beadce96f6 | ||
|
|
b1d7d210fc | ||
|
|
32e96dc0a6 | ||
|
|
b16a11c365 | ||
|
|
81f218ddc9 | ||
|
|
be6f79521e | ||
|
|
551175862e | ||
|
|
dbad5ae9c7 | ||
|
|
768a45bbb8 | ||
|
|
bbe09ced1d | ||
|
|
2e7c534a3b | ||
|
|
71684dc517 | ||
|
|
4dbb55059d | ||
|
|
f480a271b3 | ||
|
|
9c81c0ce18 | ||
|
|
b220c582cc | ||
|
|
29f8ac625a | ||
|
|
5b6c462943 |
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
|
||||
|
||||
6
.github/docker/entrypoint.sh
vendored
6
.github/docker/entrypoint.sh
vendored
@@ -21,6 +21,9 @@ 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
|
||||
@@ -38,6 +41,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
|
||||
|
||||
4
.github/docker/supervisord.conf
vendored
4
.github/docker/supervisord.conf
vendored
@@ -25,7 +25,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 +36,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
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -3,10 +3,8 @@ name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
mysql:
|
||||
|
||||
82
.github/workflows/docker-publish.yml
vendored
Normal file
82
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Docker
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
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: ghcr.io/pelican-dev/panel
|
||||
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
|
||||
10
Dockerfile
10
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,7 @@ 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
|
||||
|
||||
# Add scheduler to cron
|
||||
RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -
|
||||
@@ -49,8 +50,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
|
||||
|
||||
|
||||
45
app/Casts/EndpointCollection.php
Normal file
45
app/Casts/EndpointCollection.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class EndpointCollection implements Castable
|
||||
{
|
||||
public static function castUsing(array $arguments)
|
||||
{
|
||||
return new class() implements CastsAttributes
|
||||
{
|
||||
public function get($model, $key, $value, $attributes)
|
||||
{
|
||||
if (!isset($attributes[$key])) {
|
||||
return new Collection();
|
||||
}
|
||||
|
||||
$data = json_decode($attributes[$key], true);
|
||||
|
||||
return (new Collection($data))->map(function ($value) {
|
||||
return new Endpoint($value);
|
||||
});
|
||||
}
|
||||
|
||||
public function set($model, $key, $value, $attributes)
|
||||
{
|
||||
if (!is_array($value) && !$value instanceof Collection) {
|
||||
return new Collection();
|
||||
}
|
||||
|
||||
if (!$value instanceof Collection) {
|
||||
$value = new Collection($value);
|
||||
}
|
||||
|
||||
return [
|
||||
'ports' => $value->toJson(),
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
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';
|
||||
|
||||
@@ -51,7 +51,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,16 @@
|
||||
|
||||
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 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 +36,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();
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ 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;
|
||||
@@ -14,8 +16,11 @@ 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 +51,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,13 +60,14 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
|
||||
|
||||
return redirect()->back()->withInput();
|
||||
@@ -73,10 +79,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 +91,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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\Service\Allocation;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class AllocationDoesNotBelongToServerException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class AutoAllocationNotEnabledException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* AutoAllocationNotEnabledException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'Server auto-allocation is not enabled for this instance.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class CidrOutOfRangeException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* CidrOutOfRangeException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(trans('exceptions.allocations.cidr_out_of_range'));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class InvalidPortMappingException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* InvalidPortMappingException constructor.
|
||||
*/
|
||||
public function __construct(mixed $port)
|
||||
{
|
||||
parent::__construct(trans('exceptions.allocations.invalid_mapping', ['port' => $port]));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoAutoAllocationSpaceAvailableException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* NoAutoAllocationSpaceAvailableException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'Cannot assign additional allocation: no more space available on node.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class PortOutOfRangeException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* PortOutOfRangeException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(trans('exceptions.allocations.port_out_of_range'));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class ServerUsingAllocationException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class TooManyPortsInRangeException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* TooManyPortsInRangeException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(trans('exceptions.allocations.too_many_ports'));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Deployment;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoViableAllocationException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,17 +4,17 @@ namespace App\Extensions\Themes;
|
||||
|
||||
class Theme
|
||||
{
|
||||
public function js($path): string
|
||||
public function js(string $path): string
|
||||
{
|
||||
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
|
||||
}
|
||||
|
||||
public function css($path): string
|
||||
public function css(string $path): string
|
||||
{
|
||||
return sprintf('<link media="all" type="text/css" rel="stylesheet" href="%s"/>' . PHP_EOL, $this->getUrl($path));
|
||||
}
|
||||
|
||||
protected function getUrl($path): string
|
||||
protected function getUrl(string $path): string
|
||||
{
|
||||
return '/themes/panel/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
@@ -28,16 +28,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->versionData()['version'],
|
||||
'latestVersion' => $this->softwareVersionService->getPanel(),
|
||||
'isLatest' => $this->softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
@@ -67,7 +71,7 @@ class Dashboard extends Page
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url($softwareVersionService->getDonations(), true)
|
||||
->url($this->softwareVersionService->getDonations(), true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Filament\Pages\Installer;
|
||||
|
||||
use App\Filament\Pages\Dashboard;
|
||||
use App\Filament\Pages\Installer\Steps\AdminUserStep;
|
||||
use App\Filament\Pages\Installer\Steps\CompletedStep;
|
||||
use App\Filament\Pages\Installer\Steps\DatabaseStep;
|
||||
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
|
||||
use App\Filament\Pages\Installer\Steps\RedisStep;
|
||||
@@ -12,15 +14,17 @@ 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\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
@@ -32,56 +36,46 @@ 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';
|
||||
|
||||
private User $user;
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
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),
|
||||
RedisStep::make($this)
|
||||
->hidden(fn (Get $get) => $get('env_general.SESSION_DRIVER') != 'redis' && $get('env_general.QUEUE_CONNECTION') != 'redis' && $get('env_general.CACHE_STORE') != 'redis'),
|
||||
AdminUserStep::make($this),
|
||||
CompletedStep::make(),
|
||||
])
|
||||
->persistStepInQueryString()
|
||||
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
@@ -100,61 +94,89 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
public function submit(): RedirectResponse
|
||||
{
|
||||
return true;
|
||||
// Disable installer
|
||||
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
|
||||
|
||||
// Login user
|
||||
$this->user ??= User::all()->filter(fn ($user) => $user->isRootAdmin())->first();
|
||||
auth()->guard()->login($this->user, true);
|
||||
|
||||
// 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);
|
||||
$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): void
|
||||
{
|
||||
try {
|
||||
$userData = array_get($this->data, 'user');
|
||||
$userData['root_admin'] = true;
|
||||
$this->user = $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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
|
||||
class AdminUserStep
|
||||
{
|
||||
public static function make(): Step
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('user')
|
||||
->label('Admin User')
|
||||
@@ -26,6 +28,7 @@ class AdminUserStep
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
]);
|
||||
])
|
||||
->afterValidation(fn (UserCreationService $service) => $installer->createAdminUser($service));
|
||||
}
|
||||
}
|
||||
|
||||
34
app/Filament/Pages/Installer/Steps/CompletedStep.php
Normal file
34
app/Filament/Pages/Installer/Steps/CompletedStep.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class CompletedStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('complete')
|
||||
->label('Setup complete')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString('The setup is nearly complete!<br>As last step you need to create a new cronjob that runs every minute to process specific tasks, such as session cleanup and scheduled tasks, and also create a queue worker.')),
|
||||
TextInput::make('crontab')
|
||||
->label(new HtmlString('Run the following command to setup 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 -'),
|
||||
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'),
|
||||
Placeholder::make('')
|
||||
->content('After you finished these two last tasks you can click on "Finish" and use your new panel! Have fun!'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,91 +2,108 @@
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
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\DB;
|
||||
use PDOException;
|
||||
|
||||
class DatabaseStep
|
||||
{
|
||||
public static function make(): Step
|
||||
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')
|
||||
TextInput::make('env_database.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->columnSpanFull()
|
||||
->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(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
|
||||
->required()
|
||||
->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
|
||||
TextInput::make('env.DB_HOST')
|
||||
->default(fn (Get $get) => env('DB_DATABASE', $get('env_general.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
|
||||
TextInput::make('env_database.DB_HOST')
|
||||
->label('Database Host')
|
||||
->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_general.DB_CONNECTION') !== 'sqlite')
|
||||
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_HOST', '127.0.0.1') : null)
|
||||
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PORT')
|
||||
->label('Database Port')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your database.')
|
||||
->required()
|
||||
->required(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('DB_PORT', 3306))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_USERNAME')
|
||||
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_PORT', 3306) : null)
|
||||
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_USERNAME')
|
||||
->label('Database Username')
|
||||
->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_general.DB_CONNECTION') !== 'sqlite')
|
||||
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_USERNAME', 'pelican') : null)
|
||||
->hidden(fn (Get $get) => $get('env_general.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'),
|
||||
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_PASSWORD') : null)
|
||||
->hidden(fn (Get $get) => $get('env_general.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_general.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, string $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,14 +2,17 @@
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
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
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
@@ -17,14 +20,14 @@ class EnvironmentStep
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'sync' => 'Sync',
|
||||
'database' => 'Database',
|
||||
'sync' => 'Sync',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
@@ -34,30 +37,30 @@ class EnvironmentStep
|
||||
'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'))
|
||||
->default(url(''))
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))),
|
||||
Toggle::make('env.SESSION_SECURE_COOKIE')
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('env_general.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://') ? 'true' : 'false')),
|
||||
TextInput::make('env_general.SESSION_SECURE_COOKIE')
|
||||
->hidden()
|
||||
->default(env('SESSION_SECURE_COOKIE')),
|
||||
ToggleButtons::make('env.CACHE_STORE')
|
||||
->default(str_starts_with(url(''), 'https://') ? 'true' : 'false'),
|
||||
ToggleButtons::make('env_general.CACHE_STORE')
|
||||
->label('Cache Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
|
||||
@@ -65,7 +68,7 @@ class EnvironmentStep
|
||||
->inline()
|
||||
->options(self::CACHE_DRIVERS)
|
||||
->default(config('cache.default', 'file')),
|
||||
ToggleButtons::make('env.SESSION_DRIVER')
|
||||
ToggleButtons::make('env_general.SESSION_DRIVER')
|
||||
->label('Session Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
|
||||
@@ -73,15 +76,15 @@ class EnvironmentStep
|
||||
->inline()
|
||||
->options(self::SESSION_DRIVERS)
|
||||
->default(config('session.driver', 'file')),
|
||||
ToggleButtons::make('env.QUEUE_CONNECTION')
|
||||
ToggleButtons::make('env_general.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Sync" or "Database".')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::QUEUE_DRIVERS)
|
||||
->default(config('queue.default', 'database')),
|
||||
ToggleButtons::make('env.DB_CONNECTION')
|
||||
ToggleButtons::make('env_general.DB_CONNECTION')
|
||||
->label('Database Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
|
||||
@@ -89,6 +92,7 @@ class EnvironmentStep
|
||||
->inline()
|
||||
->options(self::DATABASE_DRIVERS)
|
||||
->default(config('database.default', 'sqlite')),
|
||||
]);
|
||||
])
|
||||
->afterValidation(fn () => $installer->writeToEnv('env_general'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
@@ -12,30 +14,32 @@ use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class RedisStep
|
||||
{
|
||||
public static function make(): Step
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('redis')
|
||||
->label('Redis')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.REDIS_HOST')
|
||||
TextInput::make('env_redis.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')
|
||||
TextInput::make('env_redis.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')
|
||||
TextInput::make('env_redis.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')
|
||||
TextInput::make('env_redis.REDIS_PASSWORD')
|
||||
->label('Redis Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password for your redis user. Can be empty.')
|
||||
@@ -43,25 +47,36 @@ class RedisStep
|
||||
->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();
|
||||
|
||||
->afterValidation(function (Get $get) use ($installer) {
|
||||
if (!self::testConnection($get('env_redis.REDIS_HOST'), $get('env_redis.REDIS_PORT'), $get('env_redis.REDIS_USERNAME'), $get('env_redis.REDIS_PASSWORD'))) {
|
||||
throw new Halt('Redis connection failed');
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_redis');
|
||||
});
|
||||
}
|
||||
|
||||
private static function testConnection(string $host, string $port, string $username, string $password): bool
|
||||
{
|
||||
try {
|
||||
config()->set('database.redis._panel_install_test', [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
Redis::connection('_panel_install_test')->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +38,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';
|
||||
@@ -92,7 +93,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')
|
||||
@@ -522,6 +522,25 @@ 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'))),
|
||||
]),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@ 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
|
||||
@@ -18,18 +22,11 @@ class ApiKeyResource extends Resource
|
||||
return static::getModel()::where('key_type', '2')->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 [
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -3,19 +3,24 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Models\Objects\Endpoint;
|
||||
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 +29,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
|
||||
@@ -50,7 +60,7 @@ class CreateDatabaseHost extends CreateRecord
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
->maxValue(Endpoint::PORT_CEIL),
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
@@ -94,10 +104,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()
|
||||
|
||||
@@ -5,15 +5,18 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Objects\Endpoint;
|
||||
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 +24,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
|
||||
@@ -46,7 +56,7 @@ class EditDatabaseHost extends EditRecord
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
->maxValue(Endpoint::PORT_CEIL),
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
@@ -97,12 +107,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
|
||||
@@ -64,7 +65,7 @@ class CreateEgg extends CreateRecord
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->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.
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary endpoint.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||
Hidden::make('script_is_privileged')
|
||||
@@ -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,8 +81,9 @@ 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.
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's endpoint.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||
Hidden::make('script_is_privileged')
|
||||
@@ -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'];
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Filament\Resources\EggResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -16,7 +15,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')
|
||||
@@ -33,11 +32,6 @@ class ServersRelationManager extends RelationManager
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
|
||||
TextColumn::make('image')
|
||||
->label('Docker Image'),
|
||||
SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +3,6 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\NodeResource\Pages;
|
||||
use App\Filament\Resources\NodeResource\RelationManagers\AllocationsRelationManager;
|
||||
use App\Filament\Resources\NodeResource\RelationManagers\NodesRelationManager;
|
||||
use App\Models\Node;
|
||||
use Filament\Resources\Resource;
|
||||
@@ -24,7 +23,6 @@ class NodeResource extends Resource
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
AllocationsRelationManager::class,
|
||||
NodesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
@@ -139,7 +140,7 @@ class CreateNode extends CreateRecord
|
||||
->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)
|
||||
->maxValue(65535)
|
||||
->maxValue(Endpoint::PORT_CEIL)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
@@ -153,7 +154,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 +220,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 +230,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')
|
||||
@@ -249,7 +245,7 @@ class CreateNode extends CreateRecord
|
||||
->columnSpan(1)
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->maxValue(Endpoint::PORT_CEIL)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
@@ -403,7 +399,7 @@ class CreateNode extends CreateRecord
|
||||
protected function getRedirectUrlParameters(): array
|
||||
{
|
||||
return [
|
||||
'tab' => '-configuration-tab',
|
||||
'tab' => '-configuration-file-tab',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use App\Services\Nodes\NodeUpdateService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
@@ -83,7 +84,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 +99,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 +131,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.')
|
||||
@@ -157,7 +156,6 @@ class EditNode extends EditRecord
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
@@ -168,11 +166,10 @@ class EditNode extends EditRecord
|
||||
->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)
|
||||
->maxValue(65535)
|
||||
->maxValue(Endpoint::PORT_CEIL)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
@@ -182,10 +179,8 @@ 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([
|
||||
@@ -235,11 +230,7 @@ class EditNode extends EditRecord
|
||||
->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'),
|
||||
->placeholder('Add Tags'),
|
||||
TextInput::make('upload_size')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->label('Upload Limit')
|
||||
@@ -253,7 +244,7 @@ class EditNode extends EditRecord
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->maxValue(Endpoint::PORT_CEIL)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
@@ -263,7 +254,7 @@ class EditNode extends EditRecord
|
||||
->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()
|
||||
->label('Use Node for deployment?')->inline()
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
@@ -446,6 +437,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 +454,7 @@ class EditNode extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
@@ -468,11 +470,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;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 +48,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(),
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Node;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @method Node getOwnerRecord()
|
||||
*/
|
||||
class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'allocations';
|
||||
|
||||
protected static ?string $icon = 'tabler-plug-connected';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('ip')
|
||||
|
||||
// Non Primary Allocations
|
||||
// ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
|
||||
|
||||
// All assigned allocations
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||
->searchable()
|
||||
->columns([
|
||||
TextColumn::make('id'),
|
||||
TextColumn::make('port')
|
||||
->searchable()
|
||||
->label('Port'),
|
||||
TextColumn::make('server.name')
|
||||
->label('Server')
|
||||
->icon('tabler-brand-docker')
|
||||
->searchable()
|
||||
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
|
||||
TextInputColumn::make('ip_alias')
|
||||
->searchable()
|
||||
->label('Alias'),
|
||||
TextInputColumn::make('ip')
|
||||
->searchable()
|
||||
->label('IP'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
|
||||
->form(fn () => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->ipAddresses())
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would have to port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
if (!str_contains($portEntry, '-')) {
|
||||
if (is_numeric($portEntry)) {
|
||||
$ports->push((int) $portEntry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not add non numerical ports
|
||||
$update = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
foreach (range($start, $end) as $i) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$update = true;
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$sortedPorts = $ports->sort()->values();
|
||||
if ($sortedPorts->all() !== $ports->all()) {
|
||||
$update = true;
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
})
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord(), $data)),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete allocation')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@
|
||||
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
|
||||
class NodesRelationManager extends RelationManager
|
||||
{
|
||||
@@ -33,11 +32,6 @@ class NodesRelationManager extends RelationManager
|
||||
->icon('tabler-egg')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
|
||||
TextColumn::make('cpu')->icon('tabler-cpu'),
|
||||
TextColumn::make('databases_count')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -3,39 +3,69 @@
|
||||
namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use App\Models\User;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use App\Services\Servers\ServerCreationService;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
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\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\Model;
|
||||
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;
|
||||
|
||||
public ?Egg $egg = null;
|
||||
|
||||
public array $ports = [];
|
||||
|
||||
public array $eggDefaultPorts = [];
|
||||
|
||||
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 +76,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 +98,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 +113,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)
|
||||
@@ -120,175 +151,11 @@ class CreateServer extends CreateRecord
|
||||
->searchable()
|
||||
->preload()
|
||||
->afterStateUpdated(function (Forms\Set $set, $state) {
|
||||
$set('allocation_id', null);
|
||||
$this->node = Node::find($state);
|
||||
})
|
||||
->required(),
|
||||
|
||||
Forms\Components\Select::make('allocation_id')
|
||||
->preload()
|
||||
->live()
|
||||
->prefixIcon('tabler-network')
|
||||
->label('Primary Allocation')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->disabled(fn (Forms\Get $get) => $get('node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->afterStateUpdated(function (Forms\Set $set) {
|
||||
$set('allocation_additional', null);
|
||||
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
|
||||
})
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->placeholder(function (Forms\Get $get) {
|
||||
$node = Node::find($get('node_id'));
|
||||
|
||||
if ($node?->allocations) {
|
||||
return 'Select an Allocation';
|
||||
}
|
||||
|
||||
return 'Create a New Allocation';
|
||||
})
|
||||
->relationship(
|
||||
'allocation',
|
||||
'ip',
|
||||
fn (Builder $query, Forms\Get $get) => $query
|
||||
->where('node_id', $get('node_id'))
|
||||
->whereNull('server_id'),
|
||||
)
|
||||
->createOptionForm(fn (Forms\Get $get) => [
|
||||
Forms\Components\TextInput::make('allocation_ip')
|
||||
->datalist(Node::find($get('node_id'))?->ipAddresses() ?? [])
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
// ->selectablePlaceholder(false)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->datalist([
|
||||
$get('name'),
|
||||
Egg::find($get('egg_id'))?->name,
|
||||
])
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
Forms\Components\TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would have to port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
if (!str_contains($portEntry, '-')) {
|
||||
if (is_numeric($portEntry)) {
|
||||
$ports->push((int) $portEntry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not add non-numerical ports
|
||||
$update = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
$range = $start <= $end ? range($start, $end) : range($end, $start);
|
||||
foreach ($range as $i) {
|
||||
if ($i > 1024 && $i <= 65535) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$update = true;
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$sortedPorts = $ports->sort()->values();
|
||||
if ($sortedPorts->all() !== $ports->all()) {
|
||||
$update = true;
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
})
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->createOptionUsing(function (array $data, Forms\Get $get): int {
|
||||
return collect(
|
||||
resolve(AssignmentService::class)->handle(Node::find($get('node_id')), $data)
|
||||
)->first();
|
||||
})
|
||||
->required(),
|
||||
|
||||
Forms\Components\Repeater::make('allocation_additional')
|
||||
->label('Additional Allocations')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->addActionLabel('Add Allocation')
|
||||
->disabled(fn (Forms\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')
|
||||
->live()
|
||||
->preload()
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->prefixIcon('tabler-network')
|
||||
->label('Additional Allocations')
|
||||
->columnSpan(2)
|
||||
->disabled(fn (Forms\Get $get) => $get('../../node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->placeholder('Select additional Allocations')
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->relationship(
|
||||
'allocations',
|
||||
'ip',
|
||||
fn (Builder $query, Forms\Get $get, Forms\Components\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 +164,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,47 +178,33 @@ class CreateServer extends CreateRecord
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Select::make('egg_id')
|
||||
Select::make('egg_id')
|
||||
->prefixIcon('tabler-egg')
|
||||
->relationship('egg', 'name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->relationship('egg', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Forms\Set $set, Forms\Get $get, $old) {
|
||||
$egg = Egg::query()->find($state);
|
||||
$set('startup', $egg->startup ?? '');
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get, $old) {
|
||||
$this->egg = Egg::query()->find($state);
|
||||
$set('startup', $this->egg?->startup);
|
||||
$set('image', '');
|
||||
|
||||
$variables = $egg->variables ?? [];
|
||||
$serverVariables = collect();
|
||||
foreach ($variables as $variable) {
|
||||
$serverVariables->add($variable->toArray());
|
||||
}
|
||||
|
||||
$variables = [];
|
||||
$set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all());
|
||||
for ($i = 0; $i < $serverVariables->count(); $i++) {
|
||||
$set("$path.$i.variable_value", $serverVariables[$i]['default_value']);
|
||||
$set("$path.$i.variable_id", $serverVariables[$i]['id']);
|
||||
$variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value'];
|
||||
}
|
||||
|
||||
$set('environment', $variables);
|
||||
$this->resetEggVariables($set, $get);
|
||||
|
||||
$previousEgg = Egg::query()->find($old);
|
||||
if (!$get('name') || $previousEgg?->getKebabName() === $get('name')) {
|
||||
$set('name', $egg->getKebabName());
|
||||
$set('name', $this->egg->getKebabName());
|
||||
}
|
||||
})
|
||||
->required(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||
ToggleButtons::make('skip_scripts')
|
||||
->label('Run Egg Install Script?')
|
||||
->default(false)
|
||||
->columnSpan([
|
||||
@@ -375,7 +228,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,16 +252,24 @@ 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 () => !$this->egg)
|
||||
->required()
|
||||
->live()
|
||||
->disabled(fn (Forms\Get $get) => $this->egg === null)
|
||||
->afterStateUpdated($this->resetEggVariables(...))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
1
|
||||
0
|
||||
);
|
||||
})
|
||||
->columnSpan([
|
||||
@@ -418,24 +279,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 +309,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 +326,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 +337,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 +353,76 @@ class CreateServer extends CreateRecord
|
||||
->columnSpan(2),
|
||||
]),
|
||||
]),
|
||||
Wizard\Step::make('Environment Configuration')
|
||||
|
||||
Wizard\Step::make('Allocation')
|
||||
->label('Allocation')
|
||||
->icon('tabler-transfer-in')
|
||||
->completedIcon('tabler-check')
|
||||
->columns(4)
|
||||
->schema([
|
||||
|
||||
Forms\Components\TagsInput::make('ports')
|
||||
->columnSpan(2)
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Ports are limited from 1025 to 65535')
|
||||
->placeholder('Example: 25565, 8080, 1337-1340')
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
You would typically port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->afterStateUpdated(self::ports(...))
|
||||
->live(),
|
||||
|
||||
Forms\Components\Repeater::make('assignments')
|
||||
->columnSpan(2)
|
||||
->defaultItems(fn () => count($this->eggDefaultPorts))
|
||||
->label('Port Assignments')
|
||||
->helperText(function (Forms\Get $get) {
|
||||
if (empty($this->eggDefaultPorts)) {
|
||||
return "This egg doesn't have any ports defined.";
|
||||
}
|
||||
|
||||
if (empty($get('ports'))) {
|
||||
return 'You must add ports to assign them!';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->live()
|
||||
->addable(false)
|
||||
->deletable(false)
|
||||
->reorderable(false)
|
||||
->simple(
|
||||
Forms\Components\Select::make('port')
|
||||
->live()
|
||||
->placeholder('Select a Port')
|
||||
->disabled(fn (Forms\Get $get) => empty($get('../../ports')) || empty($get('../../assignments')))
|
||||
->prefix(function (Forms\Components\Component $component) {
|
||||
$key = str($component->getStatePath())->beforeLast('.')->afterLast('.')->toString();
|
||||
|
||||
return $key;
|
||||
})
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->options(fn (Forms\Get $get) => $this->ports)
|
||||
->required(),
|
||||
),
|
||||
|
||||
Forms\Components\Select::make('ip')
|
||||
->label('IP Address')
|
||||
->options(fn () => collect($this->node?->ipAddresses())->mapWithKeys(fn ($ip) => [$ip => $ip]))
|
||||
->placeholder('Any')
|
||||
->columnSpan(1),
|
||||
|
||||
]),
|
||||
|
||||
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 +431,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 +450,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 +462,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 +481,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 +493,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 +512,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)
|
||||
@@ -600,23 +525,23 @@ class CreateServer extends CreateRecord
|
||||
->helperText('100% equals one CPU core.'),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('swap_support')
|
||||
ToggleButtons::make('swap_support')
|
||||
->live()
|
||||
->label('Enable 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 +557,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 +573,16 @@ 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),
|
||||
->default(config('panel.default_io_weight')),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('oom_killer')
|
||||
ToggleButtons::make('oom_killer')
|
||||
->label('OOM Killer')
|
||||
->inlineLabel()->inline()
|
||||
->default(false)
|
||||
@@ -671,12 +596,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 +611,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 +633,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 +642,10 @@ 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) {
|
||||
->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 +666,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) {
|
||||
->afterStateUpdated(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
@@ -762,13 +687,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]) ?? [])
|
||||
@@ -804,32 +729,36 @@ class CreateServer extends CreateRecord
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
|
||||
$ipAddress = $data['ip'] ?? Endpoint::INADDR_ANY;
|
||||
foreach ($data['ports'] ?? [] as $i => $port) {
|
||||
$data['ports'][$i] = (string) new Endpoint($port, $ipAddress);
|
||||
}
|
||||
|
||||
/** @var ServerCreationService $service */
|
||||
$service = resolve(ServerCreationService::class);
|
||||
foreach (array_keys($this->eggDefaultPorts) as $i => $env) {
|
||||
$data['environment'][$env] = $data['ports'][$data['assignments'][$i]];
|
||||
}
|
||||
|
||||
return $service->handle($data);
|
||||
return $this->serverCreationService->handle($data, validateVariables: false);
|
||||
}
|
||||
|
||||
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, ''
|
||||
@@ -842,4 +771,79 @@ class CreateServer extends CreateRecord
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
->all();
|
||||
}
|
||||
|
||||
public function ports(array $state, Forms\Set $set): void
|
||||
{
|
||||
$ports = collect();
|
||||
foreach ($state as $portEntry) {
|
||||
if (str_contains($portEntry, '-')) {
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, Endpoint::PORT_FLOOR);
|
||||
$end = min((int) $end, Endpoint::PORT_CEIL);
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_numeric($portEntry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ports->push((int) $portEntry);
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > Endpoint::PORT_FLOOR && $port < Endpoint::PORT_CEIL)->values();
|
||||
|
||||
$set('ports', $ports->all());
|
||||
$this->ports = $ports->all();
|
||||
}
|
||||
|
||||
public function resetEggVariables(Forms\Set $set, Forms\Get $get): void
|
||||
{
|
||||
$set('assignments', []);
|
||||
|
||||
$i = 0;
|
||||
$this->eggDefaultPorts = [];
|
||||
if (str_contains($get('startup'), '{{SERVER_PORT}}') || str_contains($this->egg->config_files, '{{server.allocations.default.port}}')) {
|
||||
$this->eggDefaultPorts['SERVER_PORT'] = null;
|
||||
$set('assignments.SERVER_PORT', ['port' => null]);
|
||||
}
|
||||
|
||||
$variables = $this->egg->variables ?? [];
|
||||
$serverVariables = collect();
|
||||
$this->ports = [];
|
||||
foreach ($variables as $variable) {
|
||||
if (in_array('port', $variable->rules)) {
|
||||
$this->eggDefaultPorts[$variable->env_variable] = $variable->default_value;
|
||||
$this->ports[] = (int) $variable->default_value;
|
||||
|
||||
$set("assignments.$variable->env_variable", ['port' => $i++]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$serverVariables->add($variable->toArray());
|
||||
}
|
||||
|
||||
$set('ports', $this->ports);
|
||||
|
||||
$variables = [];
|
||||
$set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all());
|
||||
for ($i = 0; $i < $serverVariables->count(); $i++) {
|
||||
$set("$path.$i.variable_value", $serverVariables[$i]['default_value']);
|
||||
$set("$path.$i.variable_id", $serverVariables[$i]['id']);
|
||||
$variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value'];
|
||||
}
|
||||
|
||||
$set('environment', $variables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Models\Node;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use App\Models\Database;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
@@ -23,17 +28,32 @@ use App\Models\Egg;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Services\Servers\ServerDeletionService;
|
||||
use Closure;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class EditServer extends EditRecord
|
||||
{
|
||||
public ?Node $node = null;
|
||||
|
||||
public ?Egg $egg = null;
|
||||
|
||||
public array $ports = [];
|
||||
|
||||
public array $eggDefaultPorts = [];
|
||||
|
||||
protected static string $resource = ServerResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
@@ -50,15 +70,35 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Information')
|
||||
Tab::make('Information')
|
||||
->icon('tabler-info-circle')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
Forms\Components\ToggleButtons::make('condition')
|
||||
->label('Status')
|
||||
->formatStateUsing(fn (Server $server) => $server->condition)
|
||||
->options(fn ($state) => collect(array_merge(ContainerStatus::cases(), ServerState::cases()))
|
||||
->filter(fn ($condition) => $condition->value === $state)
|
||||
->mapWithKeys(fn ($state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()])
|
||||
)
|
||||
->colors(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys(
|
||||
fn ($status) => [$status->value => $status->color()]
|
||||
))
|
||||
->icons(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys(
|
||||
fn ($status) => [$status->value => $status->icon()]
|
||||
))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
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 +115,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 +129,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 +149,11 @@ class EditServer extends EditRecord
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('Description')
|
||||
Textarea::make('description')
|
||||
->label('Notes')
|
||||
->columnSpanFull(),
|
||||
|
||||
Forms\Components\TextInput::make('uuid')
|
||||
TextInput::make('uuid')
|
||||
->hintAction(CopyAction::make())
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
@@ -123,7 +163,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 +174,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 +183,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 +194,11 @@ 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 +206,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 +225,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 +236,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 +255,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 +266,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 +285,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()
|
||||
@@ -255,15 +296,15 @@ class EditServer extends EditRecord
|
||||
->minValue(0),
|
||||
]),
|
||||
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('swap_support')
|
||||
ToggleButtons::make('swap_support')
|
||||
->live()
|
||||
->label('Enable 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 +314,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 +333,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,
|
||||
})
|
||||
@@ -310,11 +351,11 @@ class EditServer extends EditRecord
|
||||
->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('oom_killer')
|
||||
->label('OOM Killer')->inlineLabel()->inline()
|
||||
->columnSpan(2)
|
||||
->options([
|
||||
@@ -326,12 +367,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 +381,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 +405,10 @@ 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) {
|
||||
->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 +424,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) {
|
||||
->afterStateUpdated(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
@@ -406,7 +447,7 @@ class EditServer extends EditRecord
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Tabs\Tab::make('Egg')
|
||||
Tab::make('Egg')
|
||||
->icon('tabler-egg')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
@@ -415,7 +456,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 +470,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 +492,67 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->required(),
|
||||
|
||||
Forms\Components\Textarea::make('startup')
|
||||
Forms\Components\TagsInput::make('ports')
|
||||
->columnSpan(3)
|
||||
->placeholder('Example: 25565, 8080, 1337-1340')
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would typically port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->formatStateUsing(fn (Server $server) => $server->ports->map(fn ($port) => (string) $port)->all())
|
||||
->afterStateUpdated(self::ports(...))
|
||||
->live(),
|
||||
|
||||
Forms\Components\Repeater::make('portVariables')
|
||||
->label('Port Assignments')
|
||||
->columnSpan(3)
|
||||
->addable(false)
|
||||
->deletable(false)
|
||||
|
||||
->mutateRelationshipDataBeforeSaveUsing(function ($data) {
|
||||
$portIndex = $data['port'];
|
||||
unset($data['port']);
|
||||
|
||||
return [
|
||||
'variable_value' => (string) $this->ports[$portIndex],
|
||||
];
|
||||
})
|
||||
|
||||
->relationship('serverVariables', function (Builder $query) {
|
||||
$query->whereHas('variable', function (Builder $query) {
|
||||
$query->where('rules', 'like', '%port%');
|
||||
});
|
||||
})
|
||||
|
||||
->simple(
|
||||
Forms\Components\Select::make('port')
|
||||
->live()
|
||||
->disabled(fn (Forms\Get $get) => empty($get('../../ports')) || empty($get('../../assignments')))
|
||||
->prefix(function (Forms\Components\Component $component, ServerVariable $serverVariable) {
|
||||
return $serverVariable->variable->env_variable;
|
||||
})
|
||||
|
||||
->formatStateUsing(function (ServerVariable $serverVariable, Forms\Get $get) {
|
||||
return array_search($serverVariable->variable_value, array_values($get('../../ports')));
|
||||
})
|
||||
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->options(fn (Forms\Get $get) => $this->ports)
|
||||
->required(),
|
||||
)
|
||||
|
||||
->afterStateHydrated(function (Forms\Set $set, Forms\Get $get, Server $server) {
|
||||
$this->ports($ports = $get('ports'), $set);
|
||||
|
||||
foreach ($this->portOptions($server->egg) as $key => $port) {
|
||||
$set("assignments.$key", ['port' => $portIndex = array_search($port, array_values($ports))]);
|
||||
}
|
||||
}),
|
||||
|
||||
Textarea::make('startup')
|
||||
->label('Startup Command')
|
||||
->required()
|
||||
->columnSpan(6)
|
||||
@@ -462,18 +563,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 +603,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 +620,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);
|
||||
@@ -533,17 +634,17 @@ class EditServer extends EditRecord
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable?->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
|
||||
return $components;
|
||||
})
|
||||
->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"]))
|
||||
@@ -551,7 +652,7 @@ class EditServer extends EditRecord
|
||||
->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 +660,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 +671,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 +685,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,11 +716,11 @@ 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) {
|
||||
@@ -628,14 +729,14 @@ class EditServer extends EditRecord
|
||||
$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 +746,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 +757,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,14 +796,14 @@ 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()
|
||||
@@ -711,7 +812,7 @@ class EditServer extends EditRecord
|
||||
->disabled(fn (Server $server) => $server->isSuspended())
|
||||
->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)),
|
||||
])->fullWidth(),
|
||||
Forms\Components\ToggleButtons::make('')
|
||||
ToggleButtons::make('')
|
||||
->hint('This will reinstall the server with the assigned egg install script.'),
|
||||
]),
|
||||
]),
|
||||
@@ -725,13 +826,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 +842,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 +856,7 @@ class EditServer extends EditRecord
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
@@ -769,26 +873,23 @@ class EditServer extends EditRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
|
||||
|
||||
if ($component instanceof Forms\Components\Select) {
|
||||
if (collect($serverVariable->variable->rules)->contains('port')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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 +904,77 @@ class EditServer extends EditRecord
|
||||
->all();
|
||||
}
|
||||
|
||||
protected function rotatePassword(DatabasePasswordService $service, $record, $set, $get): void
|
||||
public function ports(array $state, Forms\Set $set): void
|
||||
{
|
||||
$ports = collect();
|
||||
|
||||
foreach ($state as $portEntry) {
|
||||
if (str_contains($portEntry, '-')) {
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
|
||||
try {
|
||||
$startEndpoint = new Endpoint($start);
|
||||
$endEndpoint = new Endpoint($end);
|
||||
} catch (Exception) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($startEndpoint->ip !== $endEndpoint->ip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (range($startEndpoint->port, $endEndpoint->port) as $port) {
|
||||
$ports->push(new Endpoint($port, $startEndpoint->ip));
|
||||
}
|
||||
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$ports->push($i);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$ports->push(new Endpoint($portEntry));
|
||||
} catch (Exception) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$ports = $ports->map(fn ($endpoint) => (string) $endpoint);
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$set('ports', $ports->all());
|
||||
$this->ports = $ports->all();
|
||||
}
|
||||
|
||||
public function portOptions(Egg $egg, ?string $startup = null): array
|
||||
{
|
||||
if (empty($startup)) {
|
||||
$startup = $egg->startup;
|
||||
}
|
||||
|
||||
$options = [];
|
||||
if (str_contains($startup, '{{SERVER_PORT}}')) {
|
||||
$options['SERVER_PORT'] = null;
|
||||
}
|
||||
|
||||
foreach ($egg->variables as $variable) {
|
||||
if (!in_array('port', $variable->rules)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[$variable->env_variable] = $variable->default_value;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
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,14 @@ 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\TextColumn;
|
||||
use Filament\Tables\Grouping\Group;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListServers extends ListRecords
|
||||
{
|
||||
@@ -27,64 +28,55 @@ 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')
|
||||
->label('Primary Allocation')
|
||||
->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('image')->hidden(),
|
||||
TextColumn::make('backups_count')
|
||||
->counts('backups')
|
||||
->label('Backups')
|
||||
->icon('tabler-file-download')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('ports')
|
||||
->badge()
|
||||
->separator(),
|
||||
])
|
||||
->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 +88,7 @@ class ListServers extends ListRecords
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ServerResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
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\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @method Server getOwnerRecord()
|
||||
*/
|
||||
class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'allocations';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->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')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
|
||||
->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]))
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()->label('Create Allocation')
|
||||
->createAnother(false)
|
||||
->form(fn () => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->node->ipAddresses())
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would have to port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
if (!str_contains($portEntry, '-')) {
|
||||
if (is_numeric($portEntry)) {
|
||||
$ports->push((int) $portEntry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not add non numerical ports
|
||||
$update = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
foreach (range($start, $end) as $i) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$update = true;
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$sortedPorts = $ports->sort()->values();
|
||||
if ($sortedPorts->all() !== $ports->all()) {
|
||||
$update = true;
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
})
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
|
||||
Tables\Actions\AssociateAction::make()
|
||||
->multiple()
|
||||
->associateAnother(false)
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
|
||||
->label('Add Allocation'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DissociateBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ use chillerlan\QRCode\Common\EccLevel;
|
||||
use chillerlan\QRCode\Common\Version;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use Closure;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
@@ -21,13 +23,14 @@ use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
@@ -37,6 +40,13 @@ use Illuminate\Validation\Rules\Password;
|
||||
*/
|
||||
class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
{
|
||||
private ToggleTwoFactorService $toggleTwoFactorService;
|
||||
|
||||
public function boot(ToggleTwoFactorService $toggleTwoFactorService): void
|
||||
{
|
||||
$this->toggleTwoFactorService = $toggleTwoFactorService;
|
||||
}
|
||||
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
@@ -57,7 +67,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->autofocus(),
|
||||
|
||||
TextInput::make('email')
|
||||
->prefixIcon('tabler-mail')
|
||||
->label(trans('strings.email'))
|
||||
@@ -65,7 +74,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
|
||||
TextInput::make('password')
|
||||
->label(trans('strings.password'))
|
||||
->password()
|
||||
@@ -77,7 +85,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->dehydrateStateUsing(fn ($state): string => Hash::make($state))
|
||||
->live(debounce: 500)
|
||||
->same('passwordConfirmation'),
|
||||
|
||||
TextInput::make('passwordConfirmation')
|
||||
->label(trans('strings.password_confirmation'))
|
||||
->password()
|
||||
@@ -86,13 +93,11 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->required()
|
||||
->visible(fn (Get $get): bool => filled($get('password')))
|
||||
->dehydrated(false),
|
||||
|
||||
Select::make('timezone')
|
||||
->required()
|
||||
->prefixIcon('tabler-clock-pin')
|
||||
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
|
||||
->searchable(),
|
||||
|
||||
Select::make('language')
|
||||
->label(trans('strings.language'))
|
||||
->required()
|
||||
@@ -110,8 +115,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
|
||||
Tab::make('2FA')
|
||||
->icon('tabler-shield-lock')
|
||||
->schema(function () {
|
||||
|
||||
->schema(function (TwoFactorSetupService $setupService) {
|
||||
if ($this->getUser()->use_totp) {
|
||||
return [
|
||||
Placeholder::make('2fa-already-enabled')
|
||||
@@ -129,8 +133,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
|
||||
];
|
||||
}
|
||||
/** @var TwoFactorSetupService */
|
||||
$setupService = app(TwoFactorSetupService::class);
|
||||
|
||||
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
|
||||
"users.{$this->getUser()->id}.2fa.state",
|
||||
@@ -196,16 +198,13 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->helperText('Enter your current password to verify.'),
|
||||
];
|
||||
}),
|
||||
|
||||
Tab::make('API Keys')
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Grid::make('asdf')->columns(5)->schema([
|
||||
Grid::make(5)->schema([
|
||||
Section::make('Create API Key')->columnSpan(3)->schema([
|
||||
|
||||
TextInput::make('description')
|
||||
->live(),
|
||||
|
||||
TagsInput::make('allowed_ips')
|
||||
->live()
|
||||
->splitKeys([',', ' ', 'Tab'])
|
||||
@@ -222,12 +221,10 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
);
|
||||
|
||||
Activity::event('user:api-key.create')
|
||||
->subject($token->accessToken)
|
||||
->property('identifier', $token->accessToken->identifier)
|
||||
->log();
|
||||
|
||||
$action->success();
|
||||
}),
|
||||
]),
|
||||
@@ -256,13 +253,11 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make('SSH Keys')
|
||||
->icon('tabler-lock-code')
|
||||
->schema([
|
||||
Placeholder::make('Coming soon!'),
|
||||
]),
|
||||
|
||||
Tab::make('Activity')
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
@@ -286,23 +281,21 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
];
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloquent\Model
|
||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||
{
|
||||
if ($token = $data['2facode'] ?? null) {
|
||||
/** @var ToggleTwoFactorService $service */
|
||||
$service = resolve(ToggleTwoFactorService::class);
|
||||
if (!$record instanceof User) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
$tokens = $service->handle($record, $token, true);
|
||||
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
|
||||
if ($token = $data['2facode'] ?? null) {
|
||||
$tokens = $this->toggleTwoFactorService->handle($record, $token, true);
|
||||
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), 15);
|
||||
|
||||
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
|
||||
}
|
||||
|
||||
if ($token = $data['2fa-disable-code'] ?? null) {
|
||||
/** @var ToggleTwoFactorService $service */
|
||||
$service = resolve(ToggleTwoFactorService::class);
|
||||
|
||||
$service->handle($record, $token, false);
|
||||
$this->toggleTwoFactorService->handle($record, $token, false);
|
||||
|
||||
cache()->forget("users.$record->id.2fa.state");
|
||||
}
|
||||
@@ -310,7 +303,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
return parent::handleRecordUpdate($record, $data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
public function exception(Exception $e, Closure $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
|
||||
Notification::make()
|
||||
|
||||
@@ -18,6 +18,7 @@ use Illuminate\Support\Facades\Hash;
|
||||
class EditUser extends EditRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -46,6 +47,7 @@ class EditUser extends EditRecord
|
||||
])->columns(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -78,6 +78,7 @@ class ListUsers extends ListRecords
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
@@ -110,13 +111,11 @@ class ListUsers extends ListRecords
|
||||
]),
|
||||
])
|
||||
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
||||
->action(function (array $data) {
|
||||
->action(function (array $data, UserCreationService $creationService) {
|
||||
$roles = $data['roles'];
|
||||
$roles = collect($roles)->map(fn ($role) => Role::findById($role));
|
||||
unset($data['roles']);
|
||||
|
||||
/** @var UserCreationService $creationService */
|
||||
$creationService = resolve(UserCreationService::class);
|
||||
$user = $creationService->handle($data);
|
||||
|
||||
$user->syncRoles($roles);
|
||||
|
||||
@@ -6,10 +6,10 @@ use App\Enums\ServerState;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Actions;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Actions;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ServersRelationManager extends RelationManager
|
||||
{
|
||||
@@ -31,54 +31,48 @@ class ServersRelationManager extends RelationManager
|
||||
)
|
||||
->label('Suspend All Servers')
|
||||
->color('warning')
|
||||
->action(function () use ($user) {
|
||||
->action(function (SuspensionService $suspensionService) use ($user) {
|
||||
foreach ($user->servers()->whereNot('status', ServerState::Suspended)->get() as $server) {
|
||||
resolve(SuspensionService::class)->toggle($server);
|
||||
$suspensionService->toggle($server);
|
||||
}
|
||||
}),
|
||||
|
||||
Actions\Action::make('toggleUnsuspend')
|
||||
->hidden(fn () => $user->servers()->where('status', ServerState::Suspended)->count() === 0)
|
||||
->label('Unsuspend All Servers')
|
||||
->color('primary')
|
||||
->action(function () use ($user) {
|
||||
->action(function (SuspensionService $suspensionService) use ($user) {
|
||||
foreach ($user->servers()->where('status', ServerState::Suspended)->get() as $server) {
|
||||
resolve(SuspensionService::class)->toggle($server, SuspensionService::ACTION_UNSUSPEND);
|
||||
$suspensionService->toggle($server, SuspensionService::ACTION_UNSUSPEND);
|
||||
}
|
||||
}),
|
||||
])
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
TextColumn::make('uuid')
|
||||
->hidden()
|
||||
->label('UUID')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
TextColumn::make('name')
|
||||
->icon('tabler-brand-docker')
|
||||
->label(trans('strings.name'))
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||
->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]))
|
||||
->sortable(),
|
||||
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]))
|
||||
->sortable(),
|
||||
Tables\Columns\SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('image')->hidden(),
|
||||
Tables\Columns\TextColumn::make('databases_count')
|
||||
TextColumn::make('image')->hidden(),
|
||||
TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
->label('Databases')
|
||||
->icon('tabler-database')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('backups_count')
|
||||
TextColumn::make('backups_count')
|
||||
->counts('backups')
|
||||
->label('Backups')
|
||||
->icon('tabler-file-download')
|
||||
|
||||
@@ -127,7 +127,7 @@ class EggController extends Controller
|
||||
/**
|
||||
* Normalizes a string of docker image data into the expected egg format.
|
||||
*/
|
||||
protected function normalizeDockerImages(string $input = null): array
|
||||
protected function normalizeDockerImages(?string $input = null): array
|
||||
{
|
||||
$data = array_map(fn ($value) => trim($value), explode("\n", $input ?? ''));
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Http\Controllers\Admin\Nodes;
|
||||
use Illuminate\View\View;
|
||||
use App\Models\Node;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\Allocation;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\Controllers\JavascriptInjection;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
@@ -15,6 +14,7 @@ class NodeViewController extends Controller
|
||||
use JavascriptInjection;
|
||||
|
||||
public const THRESHOLD_PERCENTAGE_LOW = 75;
|
||||
|
||||
public const THRESHOLD_PERCENTAGE_MEDIUM = 90;
|
||||
|
||||
/**
|
||||
@@ -56,32 +56,6 @@ class NodeViewController extends Controller
|
||||
return view('admin.nodes.view.configuration', compact('node'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the node allocation management page.
|
||||
*/
|
||||
public function allocations(Node $node): View
|
||||
{
|
||||
$node->setRelation(
|
||||
'allocations',
|
||||
$node->allocations()
|
||||
->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL')
|
||||
->orderByRaw('INET_ATON(ip) ASC')
|
||||
->orderBy('port')
|
||||
->with('server:id,name')
|
||||
->paginate(50)
|
||||
);
|
||||
|
||||
$this->plainInject(['node' => Collection::wrap($node)->only(['id'])]);
|
||||
|
||||
return view('admin.nodes.view.allocation', [
|
||||
'node' => $node,
|
||||
'allocations' => Allocation::query()->where('node_id', $node->id)
|
||||
->groupBy('ip')
|
||||
->orderByRaw('INET_ATON(ip) ASC')
|
||||
->get(['ip']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a listing of servers that exist for this specific node.
|
||||
*/
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Allocation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Illuminate\View\Factory as ViewFactory;
|
||||
@@ -15,11 +12,8 @@ use App\Services\Nodes\NodeUpdateService;
|
||||
use Illuminate\Cache\Repository as CacheRepository;
|
||||
use App\Services\Nodes\NodeCreationService;
|
||||
use App\Services\Nodes\NodeDeletionService;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use App\Http\Requests\Admin\Node\NodeFormRequest;
|
||||
use App\Http\Requests\Admin\Node\AllocationFormRequest;
|
||||
use App\Http\Requests\Admin\Node\AllocationAliasFormRequest;
|
||||
|
||||
class NodesController extends Controller
|
||||
{
|
||||
@@ -28,7 +22,6 @@ class NodesController extends Controller
|
||||
*/
|
||||
public function __construct(
|
||||
protected AlertsMessageBag $alert,
|
||||
protected AssignmentService $assignmentService,
|
||||
protected CacheRepository $cache,
|
||||
protected NodeCreationService $creationService,
|
||||
protected NodeDeletionService $deletionService,
|
||||
@@ -46,19 +39,6 @@ class NodesController extends Controller
|
||||
return view('admin.nodes.new');
|
||||
}
|
||||
|
||||
/**
|
||||
* Post controller to create a new node on the system.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function store(NodeFormRequest $request): RedirectResponse
|
||||
{
|
||||
$node = $this->creationService->handle($request->normalize());
|
||||
$this->alert->info(trans('admin/node.notices.node_created'))->flash();
|
||||
|
||||
return redirect()->route('admin.nodes.view.allocation', $node->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates settings for a node.
|
||||
*
|
||||
@@ -73,83 +53,6 @@ class NodesController extends Controller
|
||||
return redirect()->route('admin.nodes.view.settings', $node->id)->withInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single allocation from a node.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
|
||||
*/
|
||||
public function allocationRemoveSingle(int $node, Allocation $allocation): Response
|
||||
{
|
||||
$allocation->delete();
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple individual allocations from a node.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
|
||||
*/
|
||||
public function allocationRemoveMultiple(Request $request, int $node): Response
|
||||
{
|
||||
$allocations = $request->input('allocations');
|
||||
foreach ($allocations as $rawAllocation) {
|
||||
$allocation = new Allocation();
|
||||
$allocation->id = $rawAllocation['id'];
|
||||
$this->allocationRemoveSingle($node, $allocation);
|
||||
}
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all allocations for a specific IP at once on a node.
|
||||
*/
|
||||
public function allocationRemoveBlock(Request $request, int $node): RedirectResponse
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = Node::query()->findOrFail($node);
|
||||
$node->allocations()
|
||||
->where('ip', $request->input('ip'))
|
||||
->whereNull('server_id')
|
||||
->delete();
|
||||
|
||||
$this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')]))
|
||||
->flash();
|
||||
|
||||
return redirect()->route('admin.nodes.view.allocation', $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an alias for a specific allocation on a node.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function allocationSetAlias(AllocationAliasFormRequest $request): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$allocation = Allocation::query()->findOrFail($request->input('allocation_id'));
|
||||
$alias = (empty($request->input('alias'))) ? null : $request->input('alias');
|
||||
$allocation->update(['ip_alias' => $alias]);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new allocations on a node.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
public function createAllocation(AllocationFormRequest $request, Node $node): RedirectResponse
|
||||
{
|
||||
$this->assignmentService->handle($node, $request->normalize());
|
||||
$this->alert->success(trans('admin/node.notices.allocations_added'))->flash();
|
||||
|
||||
return redirect()->route('admin.nodes.view.allocation', $node->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a node from the system.
|
||||
*
|
||||
|
||||
@@ -36,11 +36,6 @@ class CreateServerController extends Controller
|
||||
|
||||
$eggs = Egg::with('variables')->get();
|
||||
|
||||
\JavaScript::put([
|
||||
'nodeData' => Node::getForServerCreation(),
|
||||
'eggs' => $eggs->keyBy('id'),
|
||||
]);
|
||||
|
||||
return view('admin.servers.new', [
|
||||
'eggs' => $eggs,
|
||||
'nodes' => Node::all(),
|
||||
@@ -52,7 +47,6 @@ class CreateServerController extends Controller
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(ServerFormRequest $request): RedirectResponse
|
||||
|
||||
@@ -29,8 +29,6 @@ class ServerTransferController extends Controller
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
|
||||
@@ -47,12 +47,8 @@ class ServerViewController extends Controller
|
||||
*/
|
||||
public function build(Server $server): View
|
||||
{
|
||||
$allocations = $server->node->allocations->toBase();
|
||||
|
||||
return view('admin.servers.view.build', [
|
||||
'server' => $server,
|
||||
'assigned' => $allocations->where('server_id', $server->id)->sortBy('port')->sortBy('ip'),
|
||||
'unassigned' => $allocations->where('server_id', null)->sortBy('port')->sortBy('ip'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -121,10 +117,6 @@ class ServerViewController extends Controller
|
||||
$canTransfer = true;
|
||||
}
|
||||
|
||||
\JavaScript::put([
|
||||
'nodeData' => Node::getForServerCreation(),
|
||||
]);
|
||||
|
||||
return view('admin.servers.view.manage', [
|
||||
'nodes' => Node::all(),
|
||||
'server' => $server,
|
||||
|
||||
@@ -70,7 +70,7 @@ class ServersController extends Controller
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function toggleInstall(Server $server)
|
||||
public function toggleInstall(Server $server): void
|
||||
{
|
||||
if ($server->status === ServerState::InstallFailed) {
|
||||
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
|
||||
@@ -84,8 +84,6 @@ class ServersController extends Controller
|
||||
->body(trans('admin/server.alerts.install_toggled'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +92,7 @@ class ServersController extends Controller
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function reinstallServer(Server $server)
|
||||
public function reinstallServer(Server $server): void
|
||||
{
|
||||
$this->reinstallService->handle($server);
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ abstract class ApplicationApiController extends Controller
|
||||
* Perform dependency injection of certain classes needed for core functionality
|
||||
* without littering the constructors of classes that extend this abstract.
|
||||
*/
|
||||
public function loadDependencies(Fractal $fractal, Request $request)
|
||||
public function loadDependencies(Fractal $fractal, Request $request): void
|
||||
{
|
||||
$this->fractal = $fractal;
|
||||
$this->request = $request;
|
||||
@@ -51,8 +51,7 @@ abstract class ApplicationApiController extends Controller
|
||||
*
|
||||
* @template T of \App\Transformers\Api\Application\BaseTransformer
|
||||
*
|
||||
* @param class-string<T> $abstract
|
||||
*
|
||||
* @param class-string<T> $abstract
|
||||
* @return T
|
||||
*
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Nodes;
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Models\Allocation;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use App\Transformers\Api\Application\AllocationTransformer;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Allocations\GetAllocationsRequest;
|
||||
use App\Http\Requests\Api\Application\Allocations\StoreAllocationRequest;
|
||||
use App\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest;
|
||||
|
||||
class AllocationController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* AllocationController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AssignmentService $assignmentService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the allocations that exist for a given node.
|
||||
*/
|
||||
public function index(GetAllocationsRequest $request, Node $node): array
|
||||
{
|
||||
$allocations = QueryBuilder::for($node->allocations())
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('ip'),
|
||||
AllowedFilter::exact('port'),
|
||||
'ip_alias',
|
||||
AllowedFilter::callback('server_id', function (Builder $builder, $value) {
|
||||
if (empty($value) || is_bool($value) || !ctype_digit((string) $value)) {
|
||||
return $builder->whereNull('server_id');
|
||||
}
|
||||
|
||||
return $builder->where('server_id', $value);
|
||||
}),
|
||||
])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
||||
return $this->fractal->collection($allocations)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store new allocations for a given node.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
public function store(StoreAllocationRequest $request, Node $node): JsonResponse
|
||||
{
|
||||
$this->assignmentService->handle($node, $request->validated());
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific allocation from the Panel.
|
||||
*/
|
||||
public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): JsonResponse
|
||||
{
|
||||
$allocation->delete();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Roles;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Models\Role;
|
||||
@@ -21,8 +22,8 @@ class RoleController extends ApplicationApiController
|
||||
public function index(GetRoleRequest $request): array
|
||||
{
|
||||
$roles = QueryBuilder::for(Role::query())
|
||||
->allowedFilters(['name'])
|
||||
->allowedSorts(['name'])
|
||||
->allowedFilters(['id', 'name'])
|
||||
->allowedSorts(['id', 'name'])
|
||||
->paginate($request->query('per_page') ?? 10);
|
||||
|
||||
return $this->fractal->collection($roles)
|
||||
@@ -67,6 +68,10 @@ class RoleController extends ApplicationApiController
|
||||
*/
|
||||
public function update(UpdateRoleRequest $request, Role $role): array
|
||||
{
|
||||
if ($role->isRootAdmin()) {
|
||||
throw new PanelException('Can\'t update root admin role!');
|
||||
}
|
||||
|
||||
$role->update($request->validated());
|
||||
|
||||
return $this->fractal->item($role)
|
||||
@@ -81,6 +86,10 @@ class RoleController extends ApplicationApiController
|
||||
*/
|
||||
public function delete(DeleteRoleRequest $request, Role $role): Response
|
||||
{
|
||||
if ($role->isRootAdmin()) {
|
||||
throw new PanelException('Can\'t delete root admin role!');
|
||||
}
|
||||
|
||||
$role->delete();
|
||||
|
||||
return $this->returnNoContent();
|
||||
|
||||
@@ -49,7 +49,6 @@ class ServerController extends ApplicationApiController
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
*/
|
||||
public function store(StoreServerRequest $request): JsonResponse
|
||||
{
|
||||
|
||||
@@ -69,8 +69,6 @@ class ServerManagementController extends ApplicationApiController
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Http\Requests\Api\Application\Users\DeleteUserRequest;
|
||||
use App\Http\Requests\Api\Application\Users\UpdateUserRequest;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Users\AssignUserRolesRequest;
|
||||
use App\Models\Role;
|
||||
|
||||
class UserController extends ApplicationApiController
|
||||
{
|
||||
@@ -79,9 +80,34 @@ class UserController extends ApplicationApiController
|
||||
/**
|
||||
* Assign roles to a user.
|
||||
*/
|
||||
public function roles(AssignUserRolesRequest $request, User $user): array
|
||||
public function assignRoles(AssignUserRolesRequest $request, User $user): array
|
||||
{
|
||||
$user->syncRoles($request->input('roles'));
|
||||
foreach ($request->input('roles') as $role) {
|
||||
if ($role === Role::getRootAdmin()->id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$user->assignRole($role);
|
||||
}
|
||||
|
||||
$response = $this->fractal->item($user)
|
||||
->transformWith($this->getTransformer(UserTransformer::class));
|
||||
|
||||
return $response->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes roles from a user.
|
||||
*/
|
||||
public function removeRoles(AssignUserRolesRequest $request, User $user): array
|
||||
{
|
||||
foreach ($request->input('roles') as $role) {
|
||||
if ($role === Role::getRootAdmin()->id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$user->removeRole($role);
|
||||
}
|
||||
|
||||
$response = $this->fractal->item($user)
|
||||
->transformWith($this->getTransformer(UserTransformer::class));
|
||||
|
||||
@@ -41,8 +41,7 @@ abstract class ClientApiController extends ApplicationApiController
|
||||
*
|
||||
* @template T of \App\Transformers\Api\Client\BaseClientTransformer
|
||||
*
|
||||
* @param class-string<T> $abstract
|
||||
*
|
||||
* @param class-string<T> $abstract
|
||||
* @return T
|
||||
*
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Allocation;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Transformers\Api\Client\AllocationTransformer;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Services\Allocations\FindAssignableAllocationService;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
|
||||
|
||||
class NetworkAllocationController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* NetworkAllocationController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private FindAssignableAllocationService $assignableAllocationService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all the allocations available to a server and whether
|
||||
* they are currently assigned as the primary for this server.
|
||||
*/
|
||||
public function index(GetNetworkRequest $request, Server $server): array
|
||||
{
|
||||
return $this->fractal->collection($server->allocations)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function update(UpdateAllocationRequest $request, Server $server, Allocation $allocation): array
|
||||
{
|
||||
$original = $allocation->notes;
|
||||
|
||||
$allocation->forceFill(['notes' => $request->input('notes')])->save();
|
||||
|
||||
if ($original !== $allocation->notes) {
|
||||
Activity::event('server:allocation.notes')
|
||||
->subject($allocation)
|
||||
->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes])
|
||||
->log();
|
||||
}
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, Allocation $allocation): array
|
||||
{
|
||||
$server->allocation()->associate($allocation);
|
||||
$server->save();
|
||||
|
||||
Activity::event('server:allocation.primary')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notes for the allocation for a server.
|
||||
*s.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
*/
|
||||
public function store(NewAllocationRequest $request, Server $server): array
|
||||
{
|
||||
if ($server->allocations()->count() >= $server->allocation_limit) {
|
||||
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
|
||||
}
|
||||
|
||||
$allocation = $this->assignableAllocationService->handle($server);
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an allocation from a server.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
*/
|
||||
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation): JsonResponse
|
||||
{
|
||||
// Don't allow the deletion of allocations if the server does not have an
|
||||
// allocation limit set.
|
||||
if (empty($server->allocation_limit)) {
|
||||
throw new DisplayException('You cannot delete allocations for this server: no allocation limit is set.');
|
||||
}
|
||||
|
||||
if ($allocation->id === $server->allocation_id) {
|
||||
throw new DisplayException('You cannot delete the primary allocation for this server.');
|
||||
}
|
||||
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Facades\Activity;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class SettingsController extends ClientApiController
|
||||
{
|
||||
@@ -33,7 +33,11 @@ class SettingsController extends ClientApiController
|
||||
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
||||
|
||||
$server->name = $name;
|
||||
$server->description = $description;
|
||||
|
||||
if (config('panel.editable_server_descriptions')) {
|
||||
$server->description = $description;
|
||||
}
|
||||
|
||||
$server->save();
|
||||
|
||||
if ($server->name !== $name) {
|
||||
|
||||
@@ -14,7 +14,7 @@ use App\Http\Requests\Api\Remote\ActivityEventRequest;
|
||||
|
||||
class ActivityProcessingController extends Controller
|
||||
{
|
||||
public function __invoke(ActivityEventRequest $request)
|
||||
public function __invoke(ActivityEventRequest $request): void
|
||||
{
|
||||
$tz = Carbon::now()->getTimezone();
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class ServerContainersController extends Controller
|
||||
{
|
||||
$status = fluent($request->json()->all())->get('data.new_state');
|
||||
|
||||
cache()->set("servers.$server->uuid.container.status", $status, now()->addHour());
|
||||
cache()->set("servers.$server->uuid.container.status", $status, 3600);
|
||||
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class ServerDetailsController extends Controller
|
||||
|
||||
// Avoid run-away N+1 SQL queries by preloading the relationships that are used
|
||||
// within each of the services called below.
|
||||
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables')
|
||||
$servers = Server::query()->with('egg', 'mounts', 'variables')
|
||||
->where('node_id', $node->id)
|
||||
// If you don't cast this to a string you'll end up with a stringified per_page returned in
|
||||
// the metadata, and then daemon will panic crash as a result.
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\ServerTransfer;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -53,13 +52,7 @@ class ServerTransferController extends Controller
|
||||
|
||||
/** @var \App\Models\Server $server */
|
||||
$server = $this->connection->transaction(function () use ($server, $transfer) {
|
||||
$allocations = array_merge([$transfer->old_allocation], $transfer->old_additional_allocations);
|
||||
|
||||
// Remove the old allocations for the server and re-assign the server to the new
|
||||
// primary allocation and node.
|
||||
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
|
||||
$server->update([
|
||||
'allocation_id' => $transfer->new_allocation,
|
||||
'node_id' => $transfer->new_node,
|
||||
]);
|
||||
|
||||
@@ -93,9 +86,6 @@ class ServerTransferController extends Controller
|
||||
{
|
||||
$this->connection->transaction(function () use (&$transfer) {
|
||||
$transfer->forceFill(['successful' => false])->saveOrFail();
|
||||
|
||||
$allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations);
|
||||
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
|
||||
});
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
|
||||
@@ -51,7 +51,7 @@ abstract class AbstractLoginController extends Controller
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
*/
|
||||
protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null, string $message = null)
|
||||
protected function sendFailedLoginResponse(Request $request, ?Authenticatable $user = null, ?string $message = null): never
|
||||
{
|
||||
$this->incrementLoginAttempts($request);
|
||||
$this->fireFailedLoginEvent($user, [
|
||||
@@ -91,7 +91,7 @@ abstract class AbstractLoginController extends Controller
|
||||
/**
|
||||
* Determine if the user is logging in using an email or username.
|
||||
*/
|
||||
protected function getField(string $input = null): string
|
||||
protected function getField(?string $input = null): string
|
||||
{
|
||||
return ($input && str_contains($input, '@')) ? 'email' : 'username';
|
||||
}
|
||||
@@ -99,7 +99,7 @@ abstract class AbstractLoginController extends Controller
|
||||
/**
|
||||
* Fire a failed login event.
|
||||
*/
|
||||
protected function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = [])
|
||||
protected function fireFailedLoginEvent(?Authenticatable $user = null, array $credentials = []): void
|
||||
{
|
||||
Event::dispatch(new Failed('auth', $user, $credentials));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ForgotPasswordController extends Controller
|
||||
/**
|
||||
* Get the response for a failed password reset link.
|
||||
*/
|
||||
protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse
|
||||
protected function sendResetLinkFailedResponse(Request $request, string $response): JsonResponse
|
||||
{
|
||||
// As noted in #358 we will return success even if it failed
|
||||
// to avoid pointing out that an account does or does not
|
||||
@@ -28,10 +28,8 @@ class ForgotPasswordController extends Controller
|
||||
|
||||
/**
|
||||
* Get the response for a successful password reset link.
|
||||
*
|
||||
* @param string $response
|
||||
*/
|
||||
protected function sendResetLinkResponse(Request $request, $response): JsonResponse
|
||||
protected function sendResetLinkResponse(Request $request, string $response): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => trans($response),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user