mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
1 Commits
release/v1
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0a0ece531 |
@@ -1,10 +0,0 @@
|
||||
.git
|
||||
node_modules
|
||||
vendor
|
||||
database/database.sqlite
|
||||
storage/debugbar/*.json
|
||||
storage/logs/*.log
|
||||
storage/framework/cache/data/*
|
||||
storage/framework/sessions/*
|
||||
storage/framework/testing
|
||||
storage/framework/views/*.php
|
||||
11
.env.example
11
.env.example
@@ -4,6 +4,7 @@ APP_KEY=
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://panel.test
|
||||
APP_LOCALE=en
|
||||
APP_ENVIRONMENT_ONLY=true
|
||||
|
||||
LOG_CHANNEL=daily
|
||||
LOG_STACK=single
|
||||
@@ -12,10 +13,17 @@ LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=database
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
|
||||
HASHIDS_SALT=
|
||||
HASHIDS_LENGTH=8
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_HOST=smtp.example.com
|
||||
MAIL_PORT=25
|
||||
@@ -26,7 +34,6 @@ 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
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: pelican-dev
|
||||
custom: [https://hub.pelican.dev/donors]
|
||||
custom: [https://buy.stripe.com/14kdU99SI4UT7ni9AB, https://buy.stripe.com/14kaHXc0Q9b9372eUU]
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -33,6 +33,7 @@ body:
|
||||
attributes:
|
||||
label: Panel Version
|
||||
description: Version number of your Panel (latest is not a version)
|
||||
placeholder: 1.4.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -41,6 +42,7 @@ body:
|
||||
attributes:
|
||||
label: Wings Version
|
||||
description: Version number of your Wings (latest is not a version)
|
||||
placeholder: 1.4.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -66,7 +68,7 @@ body:
|
||||
Run the following command to collect logs on your system.
|
||||
|
||||
Wings: `sudo wings diagnostics`
|
||||
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
|
||||
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | nc pelipaste.com 99`
|
||||
placeholder: "https://pelipaste.com/a1h6z"
|
||||
render: bash
|
||||
validations:
|
||||
|
||||
83
.github/docker/entrypoint.sh
vendored
83
.github/docker/entrypoint.sh
vendored
@@ -1,58 +1,81 @@
|
||||
#!/bin/ash -e
|
||||
cd /app
|
||||
|
||||
#mkdir -p /var/log/supervisord/ /var/log/php8/ \
|
||||
mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php8/ \
|
||||
&& chmod 777 /var/log/panel/logs/ \
|
||||
&& ln -s /app/storage/logs/ /var/log/panel/
|
||||
|
||||
## check for .env file and generate app keys if missing
|
||||
if [ -f /pelican-data/.env ]; then
|
||||
if [ -f /app/var/.env ]; then
|
||||
echo "external vars exist."
|
||||
rm -rf /var/www/html/.env
|
||||
rm -rf /app/.env
|
||||
ln -s /app/var/.env /app/
|
||||
else
|
||||
echo "external vars don't exist."
|
||||
rm -rf /var/www/html/.env
|
||||
touch /pelican-data/.env
|
||||
rm -rf /app/.env
|
||||
touch /app/var/.env
|
||||
|
||||
## manually generate a key because key generate --force fails
|
||||
if [ -z $APP_KEY ]; then
|
||||
echo -e "Generating key."
|
||||
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
||||
echo -e "Generated app key: $APP_KEY"
|
||||
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
|
||||
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
|
||||
else
|
||||
echo -e "APP_KEY exists in environment, using that."
|
||||
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
|
||||
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
|
||||
fi
|
||||
|
||||
ln -s /app/var/.env /app/
|
||||
fi
|
||||
|
||||
mkdir /pelican-data/database
|
||||
ln -s /pelican-data/.env /var/www/html/
|
||||
ln -s /pelican-data/database/database.sqlite /var/www/html/database/
|
||||
|
||||
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
|
||||
echo "Generating APP_KEY..."
|
||||
php artisan key:generate --force
|
||||
echo "Checking if https is required."
|
||||
if [ -f /etc/nginx/http.d/panel.conf ]; then
|
||||
echo "Using nginx config already in place."
|
||||
if [ $LE_EMAIL ]; then
|
||||
echo "Checking for cert update"
|
||||
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
|
||||
else
|
||||
echo "No letsencrypt email is set"
|
||||
fi
|
||||
else
|
||||
echo "APP_KEY is already set."
|
||||
echo "Checking if letsencrypt email is set."
|
||||
if [ -z $LE_EMAIL ]; then
|
||||
echo "No letsencrypt email is set using http config."
|
||||
cp .github/docker/default.conf /etc/nginx/http.d/panel.conf
|
||||
else
|
||||
echo "writing ssl config"
|
||||
cp .github/docker/default_ssl.conf /etc/nginx/http.d/panel.conf
|
||||
echo "updating ssl config for domain"
|
||||
sed -i "s|<domain>|$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/http.d/panel.conf
|
||||
echo "generating certs"
|
||||
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
|
||||
fi
|
||||
echo "Removing the default nginx config"
|
||||
rm -rf /etc/nginx/http.d/default.conf
|
||||
fi
|
||||
|
||||
if [[ -z $DB_PORT ]]; then
|
||||
echo -e "DB_PORT not specified, defaulting to 3306"
|
||||
DB_PORT=3306
|
||||
fi
|
||||
|
||||
## check for DB up before starting the panel
|
||||
echo "Checking database status."
|
||||
until nc -z -v -w30 $DB_HOST $DB_PORT
|
||||
do
|
||||
echo "Waiting for database connection..."
|
||||
# wait for 1 seconds before check again
|
||||
sleep 1
|
||||
done
|
||||
|
||||
## make sure the db is set up
|
||||
echo -e "Migrating Database"
|
||||
php artisan migrate --force
|
||||
echo -e "Migrating and Seeding D.B"
|
||||
php artisan migrate --seed --force
|
||||
|
||||
## start cronjobs for the queue
|
||||
echo -e "Starting cron jobs."
|
||||
crond -L /var/log/crond -l 5
|
||||
|
||||
export SUPERVISORD_CADDY=false
|
||||
|
||||
## disable caddy if SKIP_CADDY is set
|
||||
if [[ -z $SKIP_CADDY ]]; then
|
||||
echo "Starting PHP-FPM and Caddy"
|
||||
export SUPERVISORD_CADDY=true
|
||||
else
|
||||
echo "Starting PHP-FPM only"
|
||||
fi
|
||||
|
||||
chown -R www-data:www-data . /pelican-data/.env /pelican-data/database
|
||||
|
||||
echo "Starting Supervisord"
|
||||
echo -e "Starting supervisord."
|
||||
exec "$@"
|
||||
|
||||
12
.github/docker/supervisord.conf
vendored
12
.github/docker/supervisord.conf
vendored
@@ -25,15 +25,15 @@ 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
|
||||
user=www-data
|
||||
command=/usr/local/bin/php /app/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
|
||||
user=nginx
|
||||
autostart=true
|
||||
autorestart=true
|
||||
|
||||
[program:caddy]
|
||||
command=caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
autostart=%(ENV_SUPERVISORD_CADDY)s
|
||||
autorestart=%(ENV_SUPERVISORD_CADDY)s
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g 'daemon off;'
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
3
.github/workflows/build.yaml
vendored
3
.github/workflows/build.yaml
vendored
@@ -1,9 +1,6 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
92
.github/workflows/ci.yaml
vendored
92
.github/workflows/ci.yaml
vendored
@@ -1,9 +1,6 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
@@ -16,7 +13,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
database: ["mysql:8"]
|
||||
database: ["mariadb:10.2", "mysql:8"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
@@ -32,16 +29,16 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
HASHIDS_SALT: alittlebitofsalt1234
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -63,80 +60,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/phpunit tests/Unit
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/phpunit tests/Integration
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
|
||||
mariadb:
|
||||
name: MariaDB
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: testing
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
env:
|
||||
APP_ENV: testing
|
||||
APP_DEBUG: "false"
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
DB_CONNECTION: mariadb
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-${{ matrix.php }}-
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
@@ -168,14 +92,14 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
HASHIDS_SALT: alittlebitofsalt1234
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: testing.sqlite
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
DB_DATABASE: ${{ github.workspace }}/database/testing.sqlite
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -197,7 +121,7 @@ jobs:
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
|
||||
29
.github/workflows/lint.yaml
vendored
29
.github/workflows/lint.yaml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
pint:
|
||||
name: Pint
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.3"
|
||||
php-version: "8.2"
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
@@ -29,26 +29,3 @@ jobs:
|
||||
|
||||
- name: Pint
|
||||
run: vendor/bin/pint --test
|
||||
phpstan:
|
||||
name: PHPStan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.3"
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
|
||||
- name: Setup .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
|
||||
- name: PHPStan
|
||||
run: vendor/bin/phpstan --memory-limit=-1
|
||||
|
||||
27
.github/workflows/release.yaml
vendored
27
.github/workflows/release.yaml
vendored
@@ -54,12 +54,31 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||
files: |
|
||||
panel.tar.gz
|
||||
checksum.txt
|
||||
|
||||
- name: Upload release archive
|
||||
id: upload-release-archive
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: panel.tar.gz
|
||||
asset_name: panel.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
|
||||
- name: Upload release checksum
|
||||
id: upload-release-checksum
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./checksum.txt
|
||||
asset_name: checksum.txt
|
||||
asset_content_type: text/plain
|
||||
|
||||
59
.gitignore
vendored
59
.gitignore
vendored
@@ -1,28 +1,41 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/clockwork/*
|
||||
/vendor
|
||||
*.DS_Store*
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
!.env.ci
|
||||
!.env.example
|
||||
.env*
|
||||
.vagrant/*
|
||||
.vscode/*
|
||||
storage/framework/*
|
||||
/.idea
|
||||
/.vscode
|
||||
/nbproject
|
||||
/.direnv
|
||||
|
||||
node_modules
|
||||
*.log
|
||||
_ide_helper.php
|
||||
_ide_helper_models.php
|
||||
.phpstorm.meta.php
|
||||
.yarn
|
||||
public/assets/manifest.json
|
||||
/database/*.sqlite
|
||||
filament-monaco-editor/
|
||||
_ide_helper*
|
||||
/.phpstorm.meta.php
|
||||
*.sqlite
|
||||
|
||||
# For local development with docker
|
||||
# Remove if we ever put the Dockerfile in the repo
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# for image related files
|
||||
misc
|
||||
.php-cs-fixer.cache
|
||||
coverage.xml
|
||||
resources/lang/locales.js
|
||||
.phpunit.result.cache
|
||||
|
||||
/public/build
|
||||
/public/hot
|
||||
result
|
||||
docker-compose.yaml
|
||||
|
||||
public/css/filament-monaco-editor/
|
||||
|
||||
public/js/filament-monaco-editor/
|
||||
|
||||
11
Caddyfile
11
Caddyfile
@@ -1,11 +0,0 @@
|
||||
{
|
||||
email {$ADMIN_EMAIL}
|
||||
}
|
||||
|
||||
{$APP_URL} {
|
||||
root * /var/www/html/public
|
||||
encode gzip
|
||||
|
||||
php_fastcgi 127.0.0.1:9000
|
||||
file_server
|
||||
}
|
||||
85
Dockerfile
85
Dockerfile
@@ -1,58 +1,41 @@
|
||||
# Pelican Production Dockerfile
|
||||
|
||||
FROM node:20-alpine AS yarn
|
||||
#FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Stage 0:
|
||||
# Build the assets that are needed for the frontend. This build stage is then discarded
|
||||
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
|
||||
# level distribution
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
RUN yarn install --frozen-lockfile \
|
||||
&& yarn run build:production
|
||||
|
||||
RUN yarn install --frozen-lockfile && yarn run build:production
|
||||
# Stage 1:
|
||||
# Build the actual container with all of the needed PHP dependencies that will run the application.
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
COPY --from=0 /app/public/assets ./public/assets
|
||||
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev certbot certbot-nginx \
|
||||
&& docker-php-ext-configure zip \
|
||||
&& docker-php-ext-install bcmath gd intl pdo_mysql zip \
|
||||
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
||||
&& cp .env.example .env \
|
||||
&& mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache \
|
||||
&& chmod 777 -R bootstrap storage \
|
||||
&& composer install --no-dev --optimize-autoloader \
|
||||
&& rm -rf .env bootstrap/cache/*.php \
|
||||
&& mkdir -p /app/storage/logs/ \
|
||||
&& chown -R nginx:nginx .
|
||||
|
||||
FROM php:8.3-fpm-alpine
|
||||
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
RUN rm /usr/local/etc/php-fpm.conf \
|
||||
&& echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
|
||||
&& echo "0 23 * * * certbot renew --nginx --quiet" >> /var/spool/cron/crontabs/root \
|
||||
&& sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \
|
||||
&& mkdir -p /var/run/php /var/run/nginx
|
||||
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install dependencies
|
||||
RUN apk update && apk add --no-cache \
|
||||
libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev icu-dev \
|
||||
zip unzip curl \
|
||||
caddy ca-certificates supervisor \
|
||||
&& docker-php-ext-install bcmath gd intl zip opcache pcntl posix pdo_mysql
|
||||
|
||||
# Copy the Caddyfile to the container
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
# Copy the application code to the container
|
||||
COPY . .
|
||||
|
||||
COPY --from=yarn /build/public/assets ./public/assets
|
||||
|
||||
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
|
||||
|
||||
# Add scheduler to cron
|
||||
RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -
|
||||
|
||||
## supervisord config and log dir
|
||||
RUN cp .github/docker/supervisord.conf /etc/supervisord.conf && \
|
||||
mkdir /var/log/supervisord/
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
|
||||
EXPOSE 80:2019
|
||||
EXPOSE 443
|
||||
|
||||
VOLUME /pelican-data
|
||||
COPY .github/docker/default.conf /etc/nginx/http.d/default.conf
|
||||
COPY .github/docker/www.conf /usr/local/etc/php-fpm.conf
|
||||
COPY .github/docker/supervisord.conf /etc/supervisord.conf
|
||||
|
||||
EXPOSE 80 443
|
||||
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
||||
|
||||
@@ -2,49 +2,166 @@
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AppSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
public const CACHE_DRIVERS = [
|
||||
'redis' => 'Redis',
|
||||
'memcached' => 'Memcached',
|
||||
'file' => 'Filesystem (recommended)',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'redis' => 'Redis',
|
||||
'memcached' => 'Memcached',
|
||||
'database' => 'MySQL Database',
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'redis' => 'Redis',
|
||||
'database' => 'MySQL Database',
|
||||
'sync' => 'Sync (recommended)',
|
||||
];
|
||||
|
||||
protected $description = 'Configure basic environment settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:setup
|
||||
{--url= : The URL that this Panel is running on.}';
|
||||
{--new-salt : Whether or not to generate a new salt for Hashids.}
|
||||
{--url= : The URL that this Panel is running on.}
|
||||
{--cache= : The cache driver backend to use.}
|
||||
{--session= : The session driver backend to use.}
|
||||
{--queue= : The queue driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}
|
||||
{--settings-ui= : Enable or disable the settings UI.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
public function handle(): void
|
||||
/**
|
||||
* AppSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
$this->comment('Copying example .env file');
|
||||
copy($path . '.example', $path);
|
||||
}
|
||||
|
||||
if (!config('app.key')) {
|
||||
$this->comment('Generating app key');
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*
|
||||
* @throws \App\Exceptions\PanelException
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->variables['APP_TIMEZONE'] = 'UTC';
|
||||
|
||||
if (empty(config('hashids.salt')) || $this->option('new-salt')) {
|
||||
$this->variables['HASHIDS_SALT'] = str_random(20);
|
||||
}
|
||||
|
||||
$this->output->comment(__('commands.appsettings.comment.url'));
|
||||
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
|
||||
'Application URL',
|
||||
config('app.url', 'https://example.com')
|
||||
);
|
||||
|
||||
$selected = config('cache.default', 'file');
|
||||
$this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice(
|
||||
'Cache Driver',
|
||||
self::CACHE_DRIVERS,
|
||||
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('session.driver', 'file');
|
||||
$this->variables['SESSION_DRIVER'] = $this->option('session') ?? $this->choice(
|
||||
'Session Driver',
|
||||
self::SESSION_DRIVERS,
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('queue.default', 'sync');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if (!is_null($this->option('settings-ui'))) {
|
||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true';
|
||||
} else {
|
||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true';
|
||||
}
|
||||
|
||||
// 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->checkForRedis();
|
||||
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
copy($path . '.example', $path);
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
|
||||
if (!config('app.key')) {
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if redis is selected, if so, request connection details and verify them.
|
||||
*/
|
||||
private function checkForRedis()
|
||||
{
|
||||
$items = collect($this->variables)->filter(function ($item) {
|
||||
return $item === 'redis';
|
||||
});
|
||||
|
||||
// Redis was not selected, no need to continue.
|
||||
if (count($items) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->note(__('commands.appsettings.redis.note'));
|
||||
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
|
||||
'Redis Host',
|
||||
config('database.redis.default.host')
|
||||
);
|
||||
|
||||
$askForRedisPassword = true;
|
||||
if (!empty(config('database.redis.default.password'))) {
|
||||
$this->variables['REDIS_PASSWORD'] = config('database.redis.default.password');
|
||||
$askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?');
|
||||
}
|
||||
|
||||
if ($askForRedisPassword) {
|
||||
$this->output->comment(__('commands.appsettings.redis.comment'));
|
||||
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
|
||||
'Redis Password'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->variables['REDIS_PASSWORD'])) {
|
||||
$this->variables['REDIS_PASSWORD'] = 'null';
|
||||
}
|
||||
|
||||
$this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask(
|
||||
'Redis Port',
|
||||
config('database.redis.default.port')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
class CacheSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem (default)',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
protected $description = 'Configure cache settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:cache
|
||||
{--driver= : The cache driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-user= : User used to connect to redis.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* CacheSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('cache.default', 'file');
|
||||
$this->variables['CACHE_STORE'] = $this->option('driver') ?? $this->choice(
|
||||
'Cache Driver',
|
||||
self::CACHE_DRIVERS,
|
||||
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['CACHE_STORE'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
|
||||
class DatabaseSettingsCommand extends Command
|
||||
{
|
||||
@@ -13,7 +13,6 @@ class DatabaseSettingsCommand extends Command
|
||||
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite (recommended)',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
@@ -22,10 +21,10 @@ class DatabaseSettingsCommand extends Command
|
||||
protected $signature = 'p:environment:database
|
||||
{--driver= : The database driver backend to use.}
|
||||
{--database= : The database to use.}
|
||||
{--host= : The connection address for the MySQL/ MariaDB server.}
|
||||
{--port= : The connection port for the MySQL/ MariaDB server.}
|
||||
{--username= : Username to use when connecting to the MySQL/ MariaDB server.}
|
||||
{--password= : Password to use for the MySQL/ MariaDB database.}';
|
||||
{--host= : The connection address for the MySQL server.}
|
||||
{--port= : The connection port for the MySQL server.}
|
||||
{--username= : Username to use when connecting to the MySQL server.}
|
||||
{--password= : Password to use for the MySQL database.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
@@ -83,20 +82,7 @@ class DatabaseSettingsCommand extends Command
|
||||
}
|
||||
|
||||
try {
|
||||
// Test connection
|
||||
config()->set('database.connections._panel_command_test', [
|
||||
'driver' => 'mysql',
|
||||
'host' => $this->variables['DB_HOST'],
|
||||
'port' => $this->variables['DB_PORT'],
|
||||
'database' => $this->variables['DB_DATABASE'],
|
||||
'username' => $this->variables['DB_USERNAME'],
|
||||
'password' => $this->variables['DB_PASSWORD'],
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
$this->testMySQLConnection();
|
||||
} catch (\PDOException $exception) {
|
||||
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
||||
$this->output->error(__('commands.database_settings.DB_error_2'));
|
||||
@@ -107,72 +93,12 @@ class DatabaseSettingsCommand extends Command
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'mariadb') {
|
||||
$this->output->note(__('commands.database_settings.DB_HOST_note'));
|
||||
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
|
||||
'Database Host',
|
||||
config('database.connections.mariadb.host', '127.0.0.1')
|
||||
);
|
||||
|
||||
$this->variables['DB_PORT'] = $this->option('port') ?? $this->ask(
|
||||
'Database Port',
|
||||
config('database.connections.mariadb.port', 3306)
|
||||
);
|
||||
|
||||
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||
'Database Name',
|
||||
config('database.connections.mariadb.database', 'panel')
|
||||
);
|
||||
|
||||
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
|
||||
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
|
||||
'Database Username',
|
||||
config('database.connections.mariadb.username', 'pelican')
|
||||
);
|
||||
|
||||
$askForMariaDBPassword = true;
|
||||
if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) {
|
||||
$this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password');
|
||||
$askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
|
||||
}
|
||||
|
||||
if ($askForMariaDBPassword) {
|
||||
$this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password');
|
||||
}
|
||||
|
||||
try {
|
||||
// Test connection
|
||||
config()->set('database.connections._panel_command_test', [
|
||||
'driver' => 'mariadb',
|
||||
'host' => $this->variables['DB_HOST'],
|
||||
'port' => $this->variables['DB_PORT'],
|
||||
'database' => $this->variables['DB_DATABASE'],
|
||||
'username' => $this->variables['DB_USERNAME'],
|
||||
'password' => $this->variables['DB_PASSWORD'],
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
} catch (\PDOException $exception) {
|
||||
$this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
||||
$this->output->error(__('commands.database_settings.DB_error_2'));
|
||||
|
||||
if ($this->confirm(__('commands.database_settings.go_back'))) {
|
||||
$this->database->disconnect('_panel_command_test');
|
||||
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
|
||||
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||
'Database Path',
|
||||
env('DB_DATABASE', 'database.sqlite')
|
||||
config('database.connections.sqlite.database', database_path('database.sqlite'))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,4 +108,24 @@ class DatabaseSettingsCommand extends Command
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that we can connect to the provided MySQL instance and perform a selection.
|
||||
*/
|
||||
private function testMySQLConnection()
|
||||
{
|
||||
config()->set('database.connections._panel_command_test', [
|
||||
'driver' => 'mysql',
|
||||
'host' => $this->variables['DB_HOST'],
|
||||
'port' => $this->variables['DB_PORT'],
|
||||
'database' => $this->variables['DB_DATABASE'],
|
||||
'username' => $this->variables['DB_USERNAME'],
|
||||
'password' => $this->variables['DB_PASSWORD'],
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
|
||||
class EmailSettingsCommand extends Command
|
||||
{
|
||||
@@ -31,7 +31,7 @@ class EmailSettingsCommand extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$this->variables['MAIL_MAILER'] = $this->option('driver') ?? $this->choice(
|
||||
$this->variables['MAIL_DRIVER'] = $this->option('driver') ?? $this->choice(
|
||||
trans('command/messages.environment.mail.ask_driver'),
|
||||
[
|
||||
'log' => 'Log',
|
||||
@@ -41,10 +41,10 @@ class EmailSettingsCommand extends Command
|
||||
'mandrill' => 'Mandrill',
|
||||
'postmark' => 'Postmark',
|
||||
],
|
||||
env('MAIL_MAILER', env('MAIL_DRIVER', 'smtp')),
|
||||
'smtp',
|
||||
);
|
||||
|
||||
$method = 'setup' . studly_case($this->variables['MAIL_MAILER']) . 'DriverVariables';
|
||||
$method = 'setup' . studly_case($this->variables['MAIL_DRIVER']) . 'DriverVariables';
|
||||
if (method_exists($this, $method)) {
|
||||
$this->{$method}();
|
||||
}
|
||||
@@ -61,8 +61,6 @@ class EmailSettingsCommand extends Command
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->call('queue:restart');
|
||||
|
||||
$this->line('Updating stored environment configuration file.');
|
||||
$this->line('');
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
class QueueSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database (default)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
protected $description = 'Configure queue settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:queue
|
||||
{--driver= : The queue driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-user= : User used to connect to redis.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* QueueSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('driver') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class QueueWorkerServiceCommand extends Command
|
||||
{
|
||||
protected $description = 'Create the service for the queue worker.';
|
||||
|
||||
protected $signature = 'p:environment:queue-service
|
||||
{--service-name= : Name of the queue worker service.}
|
||||
{--user= : The user that PHP runs under.}
|
||||
{--group= : The group that PHP runs under.}
|
||||
{--overwrite : Force overwrite if the service file already exists.}';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
|
||||
$path = '/etc/systemd/system/' . $serviceName . '.service';
|
||||
|
||||
$fileExists = file_exists($path);
|
||||
if ($fileExists && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
|
||||
$this->line('Creation of queue worker service file aborted because service file already exists.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
|
||||
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
|
||||
|
||||
$redisUsed = config('queue.default') === 'redis' || config('session.driver') === 'redis' || config('cache.default') === 'redis';
|
||||
$afterRedis = $redisUsed ? '
|
||||
After=redis-server.service' : '';
|
||||
|
||||
$basePath = base_path();
|
||||
|
||||
$success = File::put($path, "# Pelican Queue File
|
||||
# ----------------------------------
|
||||
|
||||
[Unit]
|
||||
Description=Pelican Queue Service$afterRedis
|
||||
|
||||
[Service]
|
||||
User=$user
|
||||
Group=$group
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/php $basePath/artisan queue:work --tries=3
|
||||
StartLimitInterval=180
|
||||
StartLimitBurst=30
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
");
|
||||
|
||||
if (!$success) {
|
||||
$this->error('Error creating service file');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($fileExists) {
|
||||
$result = Process::run("systemctl restart $serviceName.service");
|
||||
if ($result->failed()) {
|
||||
$this->error('Error restarting service: ' . $result->errorOutput());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line('Queue worker service file updated successfully.');
|
||||
} else {
|
||||
$result = Process::run("systemctl enable --now $serviceName.service");
|
||||
if ($result->failed()) {
|
||||
$this->error('Error enabling service: ' . $result->errorOutput());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->line('Queue worker service file created successfully.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
class RedisSetupCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
protected $description = 'Configure the Panel to use Redis as cache, queue and session driver.';
|
||||
|
||||
protected $signature = 'p:redis:setup
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-user= : User used to connect to redis.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* RedisSetupCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->variables['CACHE_STORE'] = 'redis';
|
||||
$this->variables['QUEUE_CONNECTION'] = 'redis';
|
||||
$this->variables['SESSION_DRIVERS'] = 'redis';
|
||||
|
||||
$this->requestRedisSettings();
|
||||
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
class SessionSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem (default)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
protected $description = 'Configure session settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:session
|
||||
{--driver= : The session driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-user= : User used to connect to redis.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* SessionSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('session.driver', 'file');
|
||||
$this->variables['SESSION_DRIVER'] = $this->option('driver') ?? $this->choice(
|
||||
'Session Driver',
|
||||
self::SESSION_DRIVERS,
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['SESSION_DRIVER'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ use App\Services\Helpers\SoftwareVersionService;
|
||||
|
||||
class InfoCommand extends Command
|
||||
{
|
||||
protected $description = 'Displays the application, database, email and backup configurations along with the panel version.';
|
||||
protected $description = 'Displays the application, database, and email configurations along with the panel version.';
|
||||
|
||||
protected $signature = 'p:info';
|
||||
|
||||
/**
|
||||
* InfoCommand constructor.
|
||||
* VersionCommand constructor.
|
||||
*/
|
||||
public function __construct(private SoftwareVersionService $versionService)
|
||||
{
|
||||
@@ -26,76 +26,45 @@ class InfoCommand extends Command
|
||||
{
|
||||
$this->output->title('Version Information');
|
||||
$this->table([], [
|
||||
['Panel Version', $this->versionService->versionData()['version']],
|
||||
['Panel Version', config('app.version')],
|
||||
['Latest Version', $this->versionService->getPanel()],
|
||||
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
|
||||
], 'compact');
|
||||
|
||||
$this->output->title('Application Configuration');
|
||||
$this->table([], [
|
||||
['Environment', config('app.env') === 'production' ? config('app.env') : $this->formatText(config('app.env'), 'bg=red')],
|
||||
['Debug Mode', config('app.debug') ? $this->formatText('Yes', 'bg=red') : 'No'],
|
||||
['Application Name', config('app.name')],
|
||||
['Application URL', config('app.url')],
|
||||
['Environment', $this->formatText(config('app.env'), config('app.env') === 'production' ?: 'bg=red')],
|
||||
['Debug Mode', $this->formatText(config('app.debug') ? 'Yes' : 'No', !config('app.debug') ?: 'bg=red')],
|
||||
['Installation URL', config('app.url')],
|
||||
['Installation Directory', base_path()],
|
||||
['Cache Driver', config('cache.default')],
|
||||
['Queue Driver', config('queue.default') === 'sync' ? $this->formatText(config('queue.default'), 'bg=red') : config('queue.default')],
|
||||
['Queue Driver', config('queue.default')],
|
||||
['Session Driver', config('session.driver')],
|
||||
['Filesystem Driver', config('filesystems.default')],
|
||||
['Default Theme', config('themes.active')],
|
||||
], 'compact');
|
||||
|
||||
$this->output->title('Database Configuration');
|
||||
$driver = config('database.default');
|
||||
if ($driver === 'sqlite') {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Database', config("database.connections.$driver.database")],
|
||||
], 'compact');
|
||||
} else {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Host', config("database.connections.$driver.host")],
|
||||
['Port', config("database.connections.$driver.port")],
|
||||
['Database', config("database.connections.$driver.database")],
|
||||
['Username', config("database.connections.$driver.username")],
|
||||
], 'compact');
|
||||
}
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Host', config("database.connections.$driver.host")],
|
||||
['Port', config("database.connections.$driver.port")],
|
||||
['Database', config("database.connections.$driver.database")],
|
||||
['Username', config("database.connections.$driver.username")],
|
||||
], 'compact');
|
||||
|
||||
// TODO: Update this to handle other mail drivers
|
||||
$this->output->title('Email Configuration');
|
||||
$driver = config('mail.default');
|
||||
if ($driver === 'smtp') {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Host', config("mail.mailers.$driver.host")],
|
||||
['Port', config("mail.mailers.$driver.port")],
|
||||
['Username', config("mail.mailers.$driver.username")],
|
||||
['Encryption', config("mail.mailers.$driver.encryption")],
|
||||
['From Address', config('mail.from.address')],
|
||||
['From Name', config('mail.from.name')],
|
||||
], 'compact');
|
||||
} else {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['From Address', config('mail.from.address')],
|
||||
['From Name', config('mail.from.name')],
|
||||
], 'compact');
|
||||
}
|
||||
|
||||
$this->output->title('Backup Configuration');
|
||||
$driver = config('backups.default');
|
||||
if ($driver === 's3') {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
['Region', config("backups.disks.$driver.region")],
|
||||
['Bucket', config("backups.disks.$driver.bucket")],
|
||||
['Endpoint', config("backups.disks.$driver.endpoint")],
|
||||
['Use path style endpoint', config("backups.disks.$driver.use_path_style_endpoint") ? 'Yes' : 'No'],
|
||||
], 'compact');
|
||||
} else {
|
||||
$this->table([], [
|
||||
['Driver', $driver],
|
||||
], 'compact');
|
||||
}
|
||||
$this->table([], [
|
||||
['Driver', config('mail.default')],
|
||||
['Host', config('mail.mailers.smtp.host')],
|
||||
['Port', config('mail.mailers.smtp.port')],
|
||||
['Username', config('mail.mailers.smtp.username')],
|
||||
['From Address', config('mail.from.address')],
|
||||
['From Name', config('mail.from.name')],
|
||||
['Encryption', config('mail.mailers.smtp.encryption')],
|
||||
], 'compact');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Maintenance;
|
||||
|
||||
use App\Models\Node;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PruneImagesCommand extends Command
|
||||
{
|
||||
protected $signature = 'p:maintenance:prune-images {node?}';
|
||||
|
||||
protected $description = 'Clean up all dangling docker images to clear up disk space.';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$node = $this->argument('node');
|
||||
|
||||
if (empty($node)) {
|
||||
$nodes = Node::all();
|
||||
/** @var Node $node */
|
||||
foreach ($nodes as $node) {
|
||||
$this->cleanupImages($node);
|
||||
}
|
||||
} else {
|
||||
$this->cleanupImages((int) $node);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupImages(int|Node $node): void
|
||||
{
|
||||
if (!$node instanceof Node) {
|
||||
$node = Node::query()->findOrFail($node);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::daemon($node)
|
||||
->connectTimeout(5)
|
||||
->timeout(30)
|
||||
->delete('/api/system/docker/image/prune')
|
||||
->json() ?? [];
|
||||
|
||||
if (empty($response) || $response['ImagesDeleted'] === null) {
|
||||
$this->warn("Node {$node->id}: No images to clean up.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = count($response['ImagesDeleted']);
|
||||
|
||||
$useBinaryPrefix = config('panel.use_binary_prefix');
|
||||
$space = round($useBinaryPrefix ? $response['SpaceReclaimed'] / 1024 / 1024 : $response['SpaceReclaimed'] / 1000 / 1000, 2) . ($useBinaryPrefix ? ' MiB' : ' MB');
|
||||
|
||||
$this->info("Node {$node->id}: Cleaned up {$count} dangling docker images. ({$space})");
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,9 @@ class MakeNodeCommand extends Command
|
||||
{--overallocateMemory= : Enter the amount of ram to overallocate (% or -1 to overallocate the maximum).}
|
||||
{--maxDisk= : Set the max disk amount.}
|
||||
{--overallocateDisk= : Enter the amount of disk to overallocate (% or -1 to overallocate the maximum).}
|
||||
{--maxCpu= : Set the max cpu amount.}
|
||||
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
|
||||
{--uploadSize= : Enter the maximum upload filesize.}
|
||||
{--daemonListeningPort= : Enter the daemon listening port.}
|
||||
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
|
||||
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
|
||||
{--daemonBase= : Enter the base folder.}';
|
||||
|
||||
protected $description = 'Creates a new node on the system via the CLI.';
|
||||
@@ -57,19 +54,16 @@ class MakeNodeCommand extends Command
|
||||
$data['public'] = $this->option('public') ?? $this->confirm(__('commands.make_node.public'), true);
|
||||
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm(__('commands.make_node.behind_proxy'));
|
||||
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm(__('commands.make_node.maintenance_mode'));
|
||||
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'), '0');
|
||||
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'), '-1');
|
||||
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'), '0');
|
||||
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'), '-1');
|
||||
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'), '0');
|
||||
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'), '-1');
|
||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '256');
|
||||
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'));
|
||||
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'));
|
||||
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'));
|
||||
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'));
|
||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100');
|
||||
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
|
||||
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');
|
||||
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(__('commands.make_node.daemonSFTPAlias'), '');
|
||||
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
|
||||
|
||||
$node = $this->creationService->handle($data);
|
||||
$this->line(__('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
|
||||
$this->line(__('commands.make_node.succes1') . $data['name'] . __('commands.make_node.succes2') . $node->id . '.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands\Overrides;
|
||||
|
||||
use App\Traits\Commands\RequiresDatabaseMigrations;
|
||||
use App\Console\RequiresDatabaseMigrations;
|
||||
use Illuminate\Database\Console\Seeds\SeedCommand as BaseSeedCommand;
|
||||
|
||||
class SeedCommand extends BaseSeedCommand
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands\Overrides;
|
||||
|
||||
use App\Traits\Commands\RequiresDatabaseMigrations;
|
||||
use App\Console\RequiresDatabaseMigrations;
|
||||
use Illuminate\Foundation\Console\UpCommand as BaseUpCommand;
|
||||
|
||||
class UpCommand extends BaseUpCommand
|
||||
|
||||
@@ -24,7 +24,7 @@ class ProcessRunnableCommand extends Command
|
||||
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
|
||||
->where('is_active', true)
|
||||
->where('is_processing', false)
|
||||
->where('next_run_at', '<=', Carbon::now()->toDateTimeString())
|
||||
->whereDate('next_run_at', '<=', Carbon::now()->toDateString())
|
||||
->get();
|
||||
|
||||
if ($schedules->count() < 1) {
|
||||
@@ -62,7 +62,7 @@ class ProcessRunnableCommand extends Command
|
||||
|
||||
$this->line(trans('command/messages.schedule.output_line', [
|
||||
'schedule' => $schedule->name,
|
||||
'id' => $schedule->id,
|
||||
'hash' => $schedule->hashid,
|
||||
]));
|
||||
} catch (\Throwable|\Exception $exception) {
|
||||
logger()->error($exception, ['schedule_id' => $schedule->id]);
|
||||
|
||||
@@ -15,7 +15,7 @@ class DeleteUserCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
$search = $this->option('user') ?? $this->ask(trans('command/messages.user.search_users'));
|
||||
Assert::notEmpty($search, 'Search term should not be empty.');
|
||||
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
|
||||
|
||||
$results = User::query()
|
||||
->where('id', 'LIKE', "$search%")
|
||||
@@ -42,8 +42,6 @@ class DeleteUserCommand extends Command
|
||||
if (!$deleteUser = $this->ask(trans('command/messages.user.select_search_user'))) {
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
$deleteUser = User::query()->findOrFail($deleteUser);
|
||||
} else {
|
||||
if (count($results) > 1) {
|
||||
$this->error(trans('command/messages.user.multiple_found'));
|
||||
@@ -55,7 +53,8 @@ class DeleteUserCommand extends Command
|
||||
}
|
||||
|
||||
if ($this->confirm(trans('command/messages.user.confirm_delete')) || !$this->input->isInteractive()) {
|
||||
$deleteUser->delete();
|
||||
$user = User::query()->findOrFail($deleteUser);
|
||||
$user->delete();
|
||||
|
||||
$this->info(trans('command/messages.user.deleted'));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class MakeUserCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
try {
|
||||
DB::connection()->getPdo();
|
||||
DB::select('select 1 where 1');
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
@@ -52,7 +52,7 @@ class MakeUserCommand extends Command
|
||||
['UUID', $user->uuid],
|
||||
['Email', $user->email],
|
||||
['Username', $user->username],
|
||||
['Admin', $user->isRootAdmin() ? 'Yes' : 'No'],
|
||||
['Admin', $user->root_admin ? 'Yes' : 'No'],
|
||||
]);
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\NodeStatistics;
|
||||
use App\Models\ActivityLog;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Database\Console\PruneCommand;
|
||||
@@ -10,7 +9,6 @@ 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
|
||||
{
|
||||
@@ -32,11 +30,7 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
|
||||
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
||||
|
||||
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
|
||||
$schedule->command(PruneImagesCommand::class)->daily();
|
||||
|
||||
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
|
||||
|
||||
if (config('backups.prune_age')) {
|
||||
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Commands;
|
||||
|
||||
use App\Traits\CheckMigrationsTrait;
|
||||
use Illuminate\Console\Command;
|
||||
namespace App\Console;
|
||||
|
||||
/**
|
||||
* @mixin Command
|
||||
* @mixin \Illuminate\Console\Command
|
||||
*/
|
||||
trait RequiresDatabaseMigrations
|
||||
{
|
||||
use CheckMigrationsTrait;
|
||||
/**
|
||||
* Checks if the migrations have finished running by comparing the last migration file.
|
||||
*/
|
||||
protected function hasCompletedMigrations(): bool
|
||||
{
|
||||
/** @var \Illuminate\Database\Migrations\Migrator $migrator */
|
||||
$migrator = $this->getLaravel()->make('migrator');
|
||||
|
||||
$files = $migrator->getMigrationFiles(database_path('migrations'));
|
||||
|
||||
if (!$migrator->repositoryExists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw a massive error into the console to hopefully catch the users attention and get
|
||||
15
app/Contracts/Extensions/HashidsInterface.php
Normal file
15
app/Contracts/Extensions/HashidsInterface.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts\Extensions;
|
||||
|
||||
use Hashids\HashidsInterface as VendorHashidsInterface;
|
||||
|
||||
interface HashidsInterface extends VendorHashidsInterface
|
||||
{
|
||||
/**
|
||||
* Decode an encoded hashid and return the first result.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function decodeFirst(string $encoded, string $default = null): mixed;
|
||||
}
|
||||
@@ -6,15 +6,12 @@ enum ContainerStatus: string
|
||||
{
|
||||
// Docker Based
|
||||
case Created = 'created';
|
||||
case Starting = 'starting';
|
||||
case Running = 'running';
|
||||
case Restarting = 'restarting';
|
||||
case Exited = 'exited';
|
||||
case Paused = 'paused';
|
||||
case Dead = 'dead';
|
||||
case Removing = 'removing';
|
||||
case Stopping = 'stopping';
|
||||
case Offline = 'offline';
|
||||
|
||||
// HTTP Based
|
||||
case Missing = 'missing';
|
||||
@@ -22,17 +19,14 @@ enum ContainerStatus: string
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
|
||||
self::Created => 'tabler-heart-plus',
|
||||
self::Starting => 'tabler-heart-up',
|
||||
self::Running => 'tabler-heartbeat',
|
||||
self::Restarting => 'tabler-heart-bolt',
|
||||
self::Exited => 'tabler-heart-exclamation',
|
||||
self::Paused => 'tabler-heart-pause',
|
||||
self::Dead, self::Offline => 'tabler-heart-x',
|
||||
self::Dead => 'tabler-heart-x',
|
||||
self::Removing => 'tabler-heart-down',
|
||||
self::Missing => 'tabler-heart-search',
|
||||
self::Stopping => 'tabler-heart-minus',
|
||||
self::Missing => 'tabler-heart-question',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +34,6 @@ enum ContainerStatus: string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Created => 'primary',
|
||||
self::Starting => 'warning',
|
||||
self::Running => 'success',
|
||||
self::Restarting => 'info',
|
||||
self::Exited => 'danger',
|
||||
@@ -48,8 +41,6 @@ enum ContainerStatus: string
|
||||
self::Dead => 'danger',
|
||||
self::Removing => 'warning',
|
||||
self::Missing => 'danger',
|
||||
self::Stopping => 'warning',
|
||||
self::Offline => 'gray',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RolePermissionModels: string
|
||||
{
|
||||
case ApiKey = 'apiKey';
|
||||
case DatabaseHost = 'databaseHost';
|
||||
case Database = 'database';
|
||||
case Egg = 'egg';
|
||||
case Mount = 'mount';
|
||||
case Node = 'node';
|
||||
case Role = 'role';
|
||||
case Server = 'server';
|
||||
case User = 'user';
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RolePermissionPrefixes: string
|
||||
{
|
||||
case ViewAny = 'viewList';
|
||||
case View = 'view';
|
||||
case Create = 'create';
|
||||
case Update = 'update';
|
||||
case Delete = 'delete';
|
||||
}
|
||||
@@ -7,7 +7,9 @@ use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
@@ -48,7 +50,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
*/
|
||||
public function render(Request $request)
|
||||
{
|
||||
if ($request->is('livewire/update')) {
|
||||
if (str($request->url())->contains('livewire')) {
|
||||
Notification::make()
|
||||
->title(static::class)
|
||||
->body($this->getMessage())
|
||||
|
||||
@@ -215,7 +215,7 @@ class Handler extends ExceptionHandler
|
||||
->map(fn ($trace) => Arr::except($trace, ['args']))
|
||||
->all(),
|
||||
'previous' => Collection::make($this->extractPrevious($e))
|
||||
->map(fn ($exception) => $exception->getTrace())
|
||||
->map(fn ($exception) => $e->getTrace())
|
||||
->map(fn ($trace) => Arr::except($trace, ['args']))
|
||||
->all(),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Deployment;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoViableNodeException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Helper;
|
||||
|
||||
class CdnVersionFetchingException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -6,9 +6,9 @@ use App\Exceptions\DisplayException;
|
||||
|
||||
class TwoFactorAuthenticationTokenInvalid extends DisplayException
|
||||
{
|
||||
public string $title = 'Invalid 2FA Code';
|
||||
public string $icon = 'tabler-2fa';
|
||||
|
||||
/**
|
||||
* TwoFactorAuthenticationTokenInvalid constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('The provided two-factor authentication token was not valid.');
|
||||
|
||||
@@ -25,7 +25,7 @@ class DynamicDatabaseConnection
|
||||
'port' => $host->port,
|
||||
'database' => $database,
|
||||
'username' => $host->username,
|
||||
'password' => $host->password,
|
||||
'password' => decrypt($host->password),
|
||||
'charset' => self::DB_CHARSET,
|
||||
'collation' => self::DB_COLLATION,
|
||||
]);
|
||||
|
||||
22
app/Extensions/Hashids.php
Normal file
22
app/Extensions/Hashids.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions;
|
||||
|
||||
use Hashids\Hashids as VendorHashids;
|
||||
use App\Contracts\Extensions\HashidsInterface;
|
||||
|
||||
class Hashids extends VendorHashids implements HashidsInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function decodeFirst(string $encoded, string $default = null): mixed
|
||||
{
|
||||
$result = $this->decode($encoded);
|
||||
if (!is_array($result)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return array_first($result, null, $default);
|
||||
}
|
||||
}
|
||||
10
app/Filament/Clusters/Settings.php
Normal file
10
app/Filament/Clusters/Settings.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters;
|
||||
|
||||
use Filament\Clusters\Cluster;
|
||||
|
||||
class Settings extends Cluster
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
}
|
||||
@@ -7,7 +7,6 @@ use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
@@ -30,14 +29,8 @@ class Dashboard extends Page
|
||||
|
||||
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(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
@@ -46,17 +39,15 @@ class Dashboard extends Page
|
||||
|
||||
'devActions' => [
|
||||
CreateAction::make()
|
||||
->label('Bugs & Features')
|
||||
->label(trans('dashboard/index.sections.intro-developers.button_issues'))
|
||||
->icon('tabler-brand-github')
|
||||
->url('https://github.com/pelican-dev/panel/issues/new/choose', true)
|
||||
->color('warning'),
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-developers.button_features'))
|
||||
->icon('tabler-brand-github')
|
||||
->url('https://github.com/pelican-dev/panel/discussions', true),
|
||||
],
|
||||
'updateActions' => [
|
||||
CreateAction::make()
|
||||
->label('Read Documentation')
|
||||
->icon('tabler-clipboard-text')
|
||||
->url('https://pelican.dev/docs/panel/update', true)
|
||||
->color('warning'),
|
||||
],
|
||||
'nodeActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
|
||||
@@ -64,10 +55,14 @@ class Dashboard extends Page
|
||||
->url(route('filament.admin.resources.nodes.create')),
|
||||
],
|
||||
'supportActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_translate'))
|
||||
->icon('tabler-language')
|
||||
->url('https://crowdin.com/project/pelican-dev', true),
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url($softwareVersionService->getDonations(), true)
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
@@ -75,6 +70,11 @@ class Dashboard extends Page
|
||||
->label(trans('dashboard/index.sections.intro-help.button_docs'))
|
||||
->icon('tabler-speedboat')
|
||||
->url('https://pelican.dev/docs', true),
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-help.button_discord'))
|
||||
->icon('tabler-brand-discord')
|
||||
->url('https://discord.gg/pelican-panel', true)
|
||||
->color('blurple'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer;
|
||||
|
||||
use App\Filament\Pages\Installer\Steps\AdminUserStep;
|
||||
use App\Filament\Pages\Installer\Steps\DatabaseStep;
|
||||
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
|
||||
use App\Filament\Pages\Installer\Steps\RedisStep;
|
||||
use App\Filament\Pages\Installer\Steps\RequirementsStep;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\CheckMigrationsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
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 Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class PanelInstaller extends SimplePage implements HasForms
|
||||
{
|
||||
use CheckMigrationsTrait;
|
||||
use EnvironmentWriterTrait;
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithForms;
|
||||
|
||||
public $data = [];
|
||||
|
||||
protected static string $view = 'filament.pages.installer';
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
}
|
||||
|
||||
public static function show(): bool
|
||||
{
|
||||
if (User::count() <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config('panel.client_features.installer.enabled')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
abort_unless(self::show(), 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(),
|
||||
])
|
||||
->persistStepInQueryString()
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
size="sm"
|
||||
wire:loading.attr="disabled"
|
||||
>
|
||||
Finish
|
||||
<span wire:loading><x-filament::loading-indicator class="h-4 w-4" /></span>
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$inputs = $this->form->getState();
|
||||
|
||||
// Write variables to .env file
|
||||
$variables = array_get($inputs, 'env');
|
||||
$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')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
|
||||
class AdminUserStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('user')
|
||||
->label('Admin User')
|
||||
->schema([
|
||||
TextInput::make('user.email')
|
||||
->label('Admin E-Mail')
|
||||
->required()
|
||||
->email()
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Admin Username')
|
||||
->required()
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Admin Password')
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
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
|
||||
{
|
||||
return Step::make('database')
|
||||
->label('Database')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->columnSpanFull()
|
||||
->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.')
|
||||
->required()
|
||||
->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
|
||||
TextInput::make('env.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')
|
||||
->label('Database Port')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your database.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('DB_PORT', 3306))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_USERNAME')
|
||||
->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')
|
||||
->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'),
|
||||
])
|
||||
->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,
|
||||
]);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Set;
|
||||
|
||||
class EnvironmentStep
|
||||
{
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'sync' => 'Sync',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('environment')
|
||||
->label('Environment')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.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')
|
||||
->label('App URL')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the URL you access your Panel from.')
|
||||
->required()
|
||||
->default(config('app.url'))
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))),
|
||||
Toggle::make('env.SESSION_SECURE_COOKIE')
|
||||
->hidden()
|
||||
->default(env('SESSION_SECURE_COOKIE')),
|
||||
ToggleButtons::make('env.CACHE_STORE')
|
||||
->label('Cache Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::CACHE_DRIVERS)
|
||||
->default(config('cache.default', 'file')),
|
||||
ToggleButtons::make('env.SESSION_DRIVER')
|
||||
->label('Session Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::SESSION_DRIVERS)
|
||||
->default(config('session.driver', 'file')),
|
||||
ToggleButtons::make('env.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Sync" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::QUEUE_DRIVERS)
|
||||
->default(config('queue.default', 'database')),
|
||||
ToggleButtons::make('env.DB_CONNECTION')
|
||||
->label('Database Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::DATABASE_DRIVERS)
|
||||
->default(config('database.default', 'sqlite')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class RedisStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('redis')
|
||||
->label('Redis')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.REDIS_HOST')
|
||||
->label('Redis Host')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
|
||||
->required()
|
||||
->default(config('database.redis.default.host')),
|
||||
TextInput::make('env.REDIS_PORT')
|
||||
->label('Redis Port')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your redis server.')
|
||||
->required()
|
||||
->default(config('database.redis.default.port')),
|
||||
TextInput::make('env.REDIS_USERNAME')
|
||||
->label('Redis Username')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your redis user. Can be empty')
|
||||
->default(config('database.redis.default.username')),
|
||||
TextInput::make('env.REDIS_PASSWORD')
|
||||
->label('Redis Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password for your redis user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(config('database.redis.default.password')),
|
||||
])
|
||||
->afterValidation(function (Get $get) {
|
||||
try {
|
||||
config()->set('database.redis._panel_install_test', [
|
||||
'host' => $get('env.REDIS_HOST'),
|
||||
'username' => $get('env.REDIS_USERNAME'),
|
||||
'password' => $get('env.REDIS_PASSWORD'),
|
||||
'port' => $get('env.REDIS_PORT'),
|
||||
]);
|
||||
|
||||
Redis::connection('_panel_install_test')->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt('Redis connection failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
|
||||
class RequirementsStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0;
|
||||
|
||||
$fields = [
|
||||
Section::make('PHP Version')
|
||||
->description('8.2 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.'),
|
||||
]),
|
||||
];
|
||||
|
||||
$phpExtensions = [
|
||||
'BCMath' => extension_loaded('bcmath'),
|
||||
'cURL' => extension_loaded('curl'),
|
||||
'GD' => extension_loaded('gd'),
|
||||
'intl' => extension_loaded('intl'),
|
||||
'mbstring' => extension_loaded('mbstring'),
|
||||
'MySQL' => extension_loaded('pdo_mysql'),
|
||||
'SQLite3' => extension_loaded('pdo_sqlite'),
|
||||
'XML' => extension_loaded('xml'),
|
||||
'Zip' => extension_loaded('zip'),
|
||||
];
|
||||
$allExtensionsInstalled = !in_array(false, $phpExtensions);
|
||||
|
||||
$fields[] = Section::make('PHP Extensions')
|
||||
->description(implode(', ', array_keys($phpExtensions)))
|
||||
->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($allExtensionsInstalled ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All needed PHP Extensions are installed.')
|
||||
->visible($allExtensionsInstalled),
|
||||
Placeholder::make('')
|
||||
->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false)))
|
||||
->visible(!$allExtensionsInstalled),
|
||||
]);
|
||||
|
||||
$folderPermissions = [
|
||||
'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4) >= 755,
|
||||
'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4) >= 755,
|
||||
];
|
||||
$correctFolderPermissions = !in_array(false, $folderPermissions);
|
||||
|
||||
$fields[] = Section::make('Folder Permissions')
|
||||
->description(implode(', ', array_keys($folderPermissions)))
|
||||
->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctFolderPermissions ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All Folders have the correct permissions.')
|
||||
->visible($correctFolderPermissions),
|
||||
Placeholder::make('')
|
||||
->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false)))
|
||||
->visible(!$correctFolderPermissions),
|
||||
]);
|
||||
|
||||
return Step::make('requirements')
|
||||
->label('Server Requirements')
|
||||
->schema($fields)
|
||||
->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) {
|
||||
if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) {
|
||||
Notification::make()
|
||||
->title('Some requirements are missing!')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,578 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Backup;
|
||||
use App\Notifications\MailTested;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class Settings extends Page implements HasForms
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithHeaderActions;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static string $view = 'filament.pages.settings';
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view settings');
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Tabs::make('Tabs')
|
||||
->columns()
|
||||
->persistTabInQueryString()
|
||||
->disabled(fn () => !auth()->user()->can('update settings'))
|
||||
->tabs([
|
||||
Tab::make('general')
|
||||
->label('General')
|
||||
->icon('tabler-home')
|
||||
->schema($this->generalSettings()),
|
||||
Tab::make('recaptcha')
|
||||
->label('reCAPTCHA')
|
||||
->icon('tabler-shield')
|
||||
->schema($this->recaptchaSettings()),
|
||||
Tab::make('mail')
|
||||
->label('Mail')
|
||||
->icon('tabler-mail')
|
||||
->schema($this->mailSettings()),
|
||||
Tab::make('backup')
|
||||
->label('Backup')
|
||||
->icon('tabler-box')
|
||||
->schema($this->backupSettings()),
|
||||
Tab::make('misc')
|
||||
->label('Misc')
|
||||
->icon('tabler-tool')
|
||||
->schema($this->miscSettings()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function generalSettings(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('APP_NAME')
|
||||
->label('App Name')
|
||||
->required()
|
||||
->alphaNum()
|
||||
->default(env('APP_NAME', 'Pelican')),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label('App Favicon')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.')
|
||||
->required()
|
||||
->default(env('APP_FAVICON', '/pelican.ico')),
|
||||
Toggle::make('APP_DEBUG')
|
||||
->label('Enable Debug Mode?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
|
||||
->default(env('APP_DEBUG', config('app.debug'))),
|
||||
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
|
||||
->label('Navigation')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Sidebar',
|
||||
true => 'Topbar',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
|
||||
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
|
||||
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
|
||||
->label('Unit prefix')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Decimal Prefix (MB/ GB)',
|
||||
true => 'Binary Prefix (MiB/ GiB)',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state))
|
||||
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
|
||||
ToggleButtons::make('APP_2FA_REQUIRED')
|
||||
->label('2FA Requirement')
|
||||
->inline()
|
||||
->options([
|
||||
0 => 'Not required',
|
||||
1 => 'Required for only Admins',
|
||||
2 => 'Required for all Users',
|
||||
])
|
||||
->formatStateUsing(fn ($state): int => (int) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
|
||||
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
|
||||
TagsInput::make('TRUSTED_PROXIES')
|
||||
->label('Trusted Proxies')
|
||||
->separator()
|
||||
->splitKeys(['Tab', ' '])
|
||||
->placeholder('New IP or IP Range')
|
||||
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
|
||||
->hintActions([
|
||||
FormAction::make('clear')
|
||||
->label('Clear')
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
||||
FormAction::make('cloudflare')
|
||||
->label('Set to Cloudflare IPs')
|
||||
->icon('tabler-brand-cloudflare')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
|
||||
'173.245.48.0/20',
|
||||
'103.21.244.0/22',
|
||||
'103.22.200.0/22',
|
||||
'103.31.4.0/22',
|
||||
'141.101.64.0/18',
|
||||
'108.162.192.0/18',
|
||||
'190.93.240.0/20',
|
||||
'188.114.96.0/20',
|
||||
'197.234.240.0/22',
|
||||
'198.41.128.0/17',
|
||||
'162.158.0.0/15',
|
||||
'104.16.0.0/13',
|
||||
'104.24.0.0/14',
|
||||
'172.64.0.0/13',
|
||||
'131.0.72.0/22',
|
||||
])),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function recaptchaSettings(): array
|
||||
{
|
||||
return [
|
||||
Toggle::make('RECAPTCHA_ENABLED')
|
||||
->label('Enable reCAPTCHA?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
|
||||
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
|
||||
TextInput::make('RECAPTCHA_DOMAIN')
|
||||
->label('Domain')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
|
||||
TextInput::make('RECAPTCHA_WEBSITE_KEY')
|
||||
->label('Website Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
|
||||
TextInput::make('RECAPTCHA_SECRET_KEY')
|
||||
->label('Secret Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
|
||||
];
|
||||
}
|
||||
|
||||
private function mailSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('MAIL_MAILER')
|
||||
->label('Mail Driver')
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
'log' => 'Print mails to Log',
|
||||
'smtp' => 'SMTP Server',
|
||||
'sendmail' => 'sendmail Binary',
|
||||
'mailgun' => 'Mailgun',
|
||||
'mandrill' => 'Mandrill',
|
||||
'postmark' => 'Postmark',
|
||||
])
|
||||
->live()
|
||||
->default(env('MAIL_MAILER', config('mail.default')))
|
||||
->hintAction(
|
||||
FormAction::make('test')
|
||||
->label('Send Test Mail')
|
||||
->icon('tabler-send')
|
||||
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(function () {
|
||||
try {
|
||||
MailNotification::route('mail', auth()->user()->email)
|
||||
->notify(new MailTested(auth()->user()));
|
||||
|
||||
Notification::make()
|
||||
->title('Test Mail sent')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Test Mail failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})
|
||||
),
|
||||
Section::make('"From" Settings')
|
||||
->description('Set the Address and Name used as "From" in mails.')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('MAIL_FROM_ADDRESS')
|
||||
->label('From Address')
|
||||
->required()
|
||||
->email()
|
||||
->default(env('MAIL_FROM_ADDRESS', config('mail.from.address'))),
|
||||
TextInput::make('MAIL_FROM_NAME')
|
||||
->label('From Name')
|
||||
->required()
|
||||
->default(env('MAIL_FROM_NAME', config('mail.from.name'))),
|
||||
]),
|
||||
Section::make('SMTP Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
|
||||
->schema([
|
||||
TextInput::make('MAIL_HOST')
|
||||
->label('Host')
|
||||
->required()
|
||||
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
|
||||
TextInput::make('MAIL_PORT')
|
||||
->label('Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
|
||||
TextInput::make('MAIL_USERNAME')
|
||||
->label('Username')
|
||||
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
|
||||
TextInput::make('MAIL_PASSWORD')
|
||||
->label('Password')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(env('MAIL_PASSWORD')),
|
||||
ToggleButtons::make('MAIL_ENCRYPTION')
|
||||
->label('Encryption')
|
||||
->inline()
|
||||
->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'])
|
||||
->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))),
|
||||
]),
|
||||
Section::make('Mailgun Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
|
||||
->schema([
|
||||
TextInput::make('MAILGUN_DOMAIN')
|
||||
->label('Domain')
|
||||
->required()
|
||||
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
|
||||
TextInput::make('MAILGUN_SECRET')
|
||||
->label('Secret')
|
||||
->required()
|
||||
->default(env('MAILGUN_SECRET', config('services.mailgun.secret'))),
|
||||
TextInput::make('MAILGUN_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->required()
|
||||
->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function backupSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('APP_BACKUP_DRIVER')
|
||||
->label('Backup Driver')
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
Backup::ADAPTER_DAEMON => 'Wings',
|
||||
Backup::ADAPTER_AWS_S3 => 'S3',
|
||||
])
|
||||
->live()
|
||||
->default(env('APP_BACKUP_DRIVER', config('backups.default'))),
|
||||
Section::make('Throttles')
|
||||
->description('Configure how many backups can be created in a period. Set period to 0 to disable this throttle.')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('BACKUP_THROTTLE_LIMIT')
|
||||
->label('Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->default(config('backups.throttles.limit')),
|
||||
TextInput::make('BACKUP_THROTTLE_PERIOD')
|
||||
->label('Period')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->suffix('Seconds')
|
||||
->default(config('backups.throttles.period')),
|
||||
]),
|
||||
Section::make('S3 Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3)
|
||||
->schema([
|
||||
TextInput::make('AWS_DEFAULT_REGION')
|
||||
->label('Default Region')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.region')),
|
||||
TextInput::make('AWS_ACCESS_KEY_ID')
|
||||
->label('Access Key ID')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.key')),
|
||||
TextInput::make('AWS_SECRET_ACCESS_KEY')
|
||||
->label('Secret Access Key')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.secret')),
|
||||
TextInput::make('AWS_BACKUPS_BUCKET')
|
||||
->label('Bucket')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.bucket')),
|
||||
TextInput::make('AWS_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.endpoint')),
|
||||
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
|
||||
->label('Use path style endpoint?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('AWS_USE_PATH_STYLE_ENDPOINT', (bool) $state))
|
||||
->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function miscSettings(): array
|
||||
{
|
||||
return [
|
||||
Section::make('Automatic Allocation Creation')
|
||||
->description('Toggle if Users can create allocations via the client area.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
|
||||
->label('Allow Users to create allocations?')
|
||||
->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_CLIENT_ALLOCATIONS_ENABLED', (bool) $state))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
|
||||
->label('Starting Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
->maxValue(65535)
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_START')),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_END')
|
||||
->label('Ending Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
->maxValue(65535)
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_END')),
|
||||
]),
|
||||
Section::make('Mail Notifications')
|
||||
->description('Toggle which mail notifications should be sent to Users.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
|
||||
->label('Server Installed')
|
||||
->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_SEND_INSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
|
||||
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
|
||||
->label('Server Reinstalled')
|
||||
->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_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
]),
|
||||
Section::make('Connections')
|
||||
->description('Timeouts used when making requests.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('GUZZLE_TIMEOUT')
|
||||
->label('Request Timeout')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(15)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->default(env('GUZZLE_TIMEOUT', config('panel.guzzle.timeout'))),
|
||||
TextInput::make('GUZZLE_CONNECT_TIMEOUT')
|
||||
->label('Connect Timeout')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(5)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->default(env('GUZZLE_CONNECT_TIMEOUT', config('panel.guzzle.connect_timeout'))),
|
||||
]),
|
||||
Section::make('Activity Logs')
|
||||
->description('Configure how often old activity logs should be pruned and whether admin activities should be logged.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_ACTIVITY_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->default(env('APP_ACTIVITY_PRUNE_DAYS', config('activity.prune_days'))),
|
||||
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
|
||||
->label('Hide admin activities?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state))
|
||||
->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))),
|
||||
]),
|
||||
Section::make('API')
|
||||
->description('Defines the rate limit for the number of requests per minute that can be executed.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_API_CLIENT_RATELIMIT')
|
||||
->label('Client API Rate Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
|
||||
TextInput::make('APP_API_APPLICATION_RATELIMIT')
|
||||
->label('Application API Rate Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
try {
|
||||
$data = $this->form->getState();
|
||||
|
||||
// Convert bools to a string, so they are correctly written to the .env file
|
||||
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data);
|
||||
|
||||
$this->writeToEnvironment($data);
|
||||
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('queue:restart');
|
||||
|
||||
$this->rememberData();
|
||||
|
||||
$this->redirect($this->getUrl());
|
||||
|
||||
Notification::make()
|
||||
->title('Settings saved')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Save failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('save')
|
||||
->action('save')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,36 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\ApiKeyResource\Pages;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ApiKeyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ApiKey::class;
|
||||
protected static ?string $label = 'API Key';
|
||||
protected static ?string $navigationIcon = 'tabler-key';
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::where('key_type', '2')->count() ?: null;
|
||||
}
|
||||
protected static ?string $navigationIcon = 'tabler-key';
|
||||
|
||||
public static function canEdit($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
return [
|
||||
'all' => Tab::make('All Keys'),
|
||||
'application' => Tab::make('Application Keys')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_APPLICATION)),
|
||||
];
|
||||
}
|
||||
|
||||
public function getDefaultActiveTab(): string|int|null
|
||||
{
|
||||
return 'application';
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -4,13 +4,9 @@ namespace App\Filament\Resources\ApiKeyResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ApiKeyResource;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Forms;
|
||||
|
||||
class CreateApiKey extends CreateRecord
|
||||
{
|
||||
@@ -22,26 +18,40 @@ class CreateApiKey extends CreateRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
|
||||
Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
|
||||
Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
|
||||
Forms\Components\Hidden::make('token')->default(encrypt(str_random(ApiKey::KEY_LENGTH))),
|
||||
|
||||
Hidden::make('user_id')
|
||||
Forms\Components\Hidden::make('user_id')
|
||||
->default(auth()->user()->id)
|
||||
->required(),
|
||||
|
||||
Hidden::make('key_type')
|
||||
Forms\Components\Select::make('key_type')
|
||||
->inlineLabel()
|
||||
->default(ApiKey::TYPE_APPLICATION)
|
||||
->required(),
|
||||
->options(function (ApiKey $apiKey) {
|
||||
$originalOptions = [
|
||||
//ApiKey::TYPE_NONE => 'None',
|
||||
ApiKey::TYPE_ACCOUNT => 'Account',
|
||||
ApiKey::TYPE_APPLICATION => 'Application',
|
||||
//ApiKey::TYPE_DAEMON_USER => 'Daemon User',
|
||||
//ApiKey::TYPE_DAEMON_APPLICATION => 'Daemon Application',
|
||||
];
|
||||
|
||||
Fieldset::make('Permissions')
|
||||
return collect($originalOptions)
|
||||
->filter(fn ($value, $key) => $key <= ApiKey::TYPE_APPLICATION || $apiKey->key_type === $key)
|
||||
->all();
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->required()
|
||||
->default(ApiKey::TYPE_APPLICATION),
|
||||
|
||||
Forms\Components\Fieldset::make('Permissions')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
])
|
||||
->schema(
|
||||
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
|
||||
collect(ApiKey::RESOURCES)->map(fn ($resource) => Forms\Components\ToggleButtons::make("r_$resource")
|
||||
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||
->options([
|
||||
0 => 'None',
|
||||
@@ -71,13 +81,15 @@ class CreateApiKey extends CreateRecord
|
||||
)->all(),
|
||||
),
|
||||
|
||||
TagsInput::make('allowed_ips')
|
||||
Forms\Components\TagsInput::make('allowed_ips')
|
||||
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
||||
->label('Whitelisted IPv4 Addresses')
|
||||
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
|
||||
->columnSpanFull(),
|
||||
->columnSpanFull()
|
||||
->hidden()
|
||||
->default(null),
|
||||
|
||||
Textarea::make('memo')
|
||||
Forms\Components\Textarea::make('memo')
|
||||
->required()
|
||||
->label('Description')
|
||||
->helperText('
|
||||
|
||||
@@ -5,10 +5,11 @@ namespace App\Filament\Resources\ApiKeyResource\Pages;
|
||||
use App\Filament\Resources\ApiKeyResource;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Components\Tab;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListApiKeys extends ListRecords
|
||||
{
|
||||
@@ -18,39 +19,42 @@ class ListApiKeys extends ListRecords
|
||||
{
|
||||
return $table
|
||||
->searchable(false)
|
||||
->modifyQueryUsing(fn ($query) => $query->where('key_type', ApiKey::TYPE_APPLICATION))
|
||||
->columns([
|
||||
TextColumn::make('key')
|
||||
Tables\Columns\TextColumn::make('user.username')
|
||||
->hidden()
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('key')
|
||||
->copyable()
|
||||
->icon('tabler-clipboard-text')
|
||||
->state(fn (ApiKey $key) => $key->identifier . $key->token),
|
||||
->state(fn (ApiKey $key) => $key->identifier . decrypt($key->token)),
|
||||
|
||||
TextColumn::make('memo')
|
||||
Tables\Columns\TextColumn::make('memo')
|
||||
->label('Description')
|
||||
->wrap()
|
||||
->limit(50),
|
||||
|
||||
TextColumn::make('identifier')
|
||||
Tables\Columns\TextColumn::make('identifier')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('last_used_at')
|
||||
Tables\Columns\TextColumn::make('last_used_at')
|
||||
->label('Last Used')
|
||||
->placeholder('Not Used')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('user.username')
|
||||
->label('Created By')
|
||||
->url(fn (ApiKey $apiKey): string => route('filament.admin.resources.users.edit', ['record' => $apiKey->user])),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
//Tables\Actions\EditAction::make()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -60,4 +64,22 @@ class ListApiKeys extends ListRecords
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTabs(): array
|
||||
{
|
||||
return [
|
||||
'all' => Tab::make('All Keys'),
|
||||
'application' => Tab::make('Application Keys')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_APPLICATION)
|
||||
),
|
||||
'account' => Tab::make('Account Keys')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_ACCOUNT)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function getDefaultActiveTab(): string|int|null
|
||||
{
|
||||
return 'application';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,6 @@ class DatabaseHostResource extends Resource
|
||||
protected static ?string $label = 'Databases';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
|
||||
@@ -3,16 +3,10 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Services\Databases\Hosts\HostCreationService;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDOException;
|
||||
|
||||
class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
@@ -28,87 +22,55 @@ class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
]),
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live()
|
||||
->debounce(500)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
return [
|
||||
$this->getCreateFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
return resolve(HostCreationService::class)->handle($data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof PDOException) {
|
||||
Notification::make()
|
||||
->title('Error connecting to database host')
|
||||
->body($e->getMessage())
|
||||
->color('danger')
|
||||
->icon('tabler-database')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$stopPropagation();
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = encrypt($data['password']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,11 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Services\Databases\Hosts\HostUpdateService;
|
||||
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\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDOException;
|
||||
|
||||
class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
@@ -25,95 +17,61 @@ class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->columnSpan(2)
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(255),
|
||||
TextInput::make('port')
|
||||
->columnSpan(1)
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
->numeric(),
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255),
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
]),
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('host')
|
||||
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||
->required()
|
||||
->live()
|
||||
->debounce(500)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('port')
|
||||
->helperText('The port that MySQL is running on for this host.')
|
||||
->required()
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
Forms\Components\TextInput::make('username')
|
||||
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||
->required()
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(191)
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||
->required()
|
||||
->maxLength(60),
|
||||
Forms\Components\Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
])->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? 'Database Host Has Databases' : 'Delete')
|
||||
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
DatabasesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate($record, array $data): Model
|
||||
{
|
||||
return resolve(HostUpdateService::class)->handle($record->id, $data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof PDOException) {
|
||||
Notification::make()
|
||||
->title('Error connecting to database host')
|
||||
->body($e->getMessage())
|
||||
->color('danger')
|
||||
->icon('tabler-database')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$stopPropagation();
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = encrypt($data['password']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,45 +5,42 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListDatabaseHosts extends ListRecords
|
||||
{
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected ?string $heading = 'Database Hosts';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('host')
|
||||
Tables\Columns\TextColumn::make('host')
|
||||
->searchable(),
|
||||
TextColumn::make('port')
|
||||
Tables\Columns\TextColumn::make('port')
|
||||
->sortable(),
|
||||
TextColumn::make('username')
|
||||
Tables\Columns\TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('max_databases')
|
||||
Tables\Columns\TextColumn::make('max_databases')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('node.name')
|
||||
Tables\Columns\TextColumn::make('node.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete databasehost')),
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -51,7 +48,7 @@ class ListDatabaseHosts extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make('create')->label('New Database Host'),
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
|
||||
|
||||
use App\Models\Database;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class DatabasesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'databases';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('database')->columnSpanFull(),
|
||||
TextInput::make('username'),
|
||||
TextInput::make('password')
|
||||
->hintAction(
|
||||
Action::make('rotate')
|
||||
->icon('tabler-refresh')
|
||||
->requiresConfirmation()
|
||||
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
|
||||
)
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')->label('Connections From'),
|
||||
TextInput::make('max_connections'),
|
||||
TextInput::make('JDBC')
|
||||
->label('JDBC Connection String')
|
||||
->columnSpanFull()
|
||||
->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
|
||||
->recordTitleAttribute('servers')
|
||||
->columns([
|
||||
TextColumn::make('database')->icon('tabler-database'),
|
||||
TextColumn::make('username')->icon('tabler-user'),
|
||||
TextColumn::make('remote'),
|
||||
TextColumn::make('server.name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
|
||||
TextColumn::make('max_connections'),
|
||||
TextColumn::make('created_at')->dateTime(),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
ViewAction::make()->color('primary'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void
|
||||
{
|
||||
$newPassword = $service->handle($database);
|
||||
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database');
|
||||
|
||||
$set('password', $newPassword);
|
||||
$set('JDBC', $jdbcString);
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,6 @@ 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
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseResource;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Forms;
|
||||
|
||||
class CreateDatabase extends CreateRecord
|
||||
{
|
||||
@@ -16,29 +15,29 @@ class CreateDatabase extends CreateRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Select::make('server_id')
|
||||
Forms\Components\Select::make('server_id')
|
||||
->relationship('server', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
TextInput::make('database_host_id')
|
||||
Forms\Components\TextInput::make('database_host_id')
|
||||
->required()
|
||||
->numeric(),
|
||||
TextInput::make('database')
|
||||
Forms\Components\TextInput::make('database')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('remote')
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('remote')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->default('%'),
|
||||
TextInput::make('username')
|
||||
Forms\Components\TextInput::make('username')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->password()
|
||||
->revealable()
|
||||
->required(),
|
||||
TextInput::make('max_connections')
|
||||
Forms\Components\TextInput::make('max_connections')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
|
||||
@@ -4,10 +4,9 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms;
|
||||
|
||||
class EditDatabase extends EditRecord
|
||||
{
|
||||
@@ -17,29 +16,29 @@ class EditDatabase extends EditRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Select::make('server_id')
|
||||
Forms\Components\Select::make('server_id')
|
||||
->relationship('server', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
TextInput::make('database_host_id')
|
||||
Forms\Components\TextInput::make('database_host_id')
|
||||
->required()
|
||||
->numeric(),
|
||||
TextInput::make('database')
|
||||
Forms\Components\TextInput::make('database')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('remote')
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('remote')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->default('%'),
|
||||
TextInput::make('username')
|
||||
Forms\Components\TextInput::make('username')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('password')
|
||||
->password()
|
||||
->revealable()
|
||||
->required(),
|
||||
TextInput::make('max_connections')
|
||||
Forms\Components\TextInput::make('max_connections')
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0),
|
||||
|
||||
@@ -5,11 +5,8 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
use App\Filament\Resources\DatabaseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListDatabases extends ListRecords
|
||||
{
|
||||
@@ -19,37 +16,39 @@ class ListDatabases extends ListRecords
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('server.name')
|
||||
Tables\Columns\TextColumn::make('server.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('database_host_id')
|
||||
Tables\Columns\TextColumn::make('database_host_id')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('database')
|
||||
Tables\Columns\TextColumn::make('database')
|
||||
->searchable(),
|
||||
TextColumn::make('username')
|
||||
Tables\Columns\TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('remote')
|
||||
Tables\Columns\TextColumn::make('remote')
|
||||
->searchable(),
|
||||
TextColumn::make('max_connections')
|
||||
Tables\Columns\TextColumn::make('max_connections')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('updated_at')
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete database')),
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -16,11 +16,6 @@ class EggResource extends Resource
|
||||
|
||||
protected static ?string $recordRouteKeyName = 'id';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -3,18 +3,6 @@
|
||||
namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EggResource;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use Filament\Forms;
|
||||
@@ -31,26 +19,25 @@ class CreateEgg extends CreateRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('Configuration')
|
||||
Forms\Components\Tabs::make()->tabs([
|
||||
Forms\Components\Tabs\Tab::make('Configuration')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||
TextInput::make('author')
|
||||
->maxLength(255)
|
||||
Forms\Components\TextInput::make('author')
|
||||
->maxLength(191)
|
||||
->required()
|
||||
->email()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('The author of this version of the Egg.'),
|
||||
Textarea::make('description')
|
||||
Forms\Components\Textarea::make('description')
|
||||
->rows(3)
|
||||
->columnSpanFull()
|
||||
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
|
||||
Textarea::make('startup')
|
||||
Forms\Components\Textarea::make('startup')
|
||||
->rows(3)
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
@@ -58,27 +45,26 @@ class CreateEgg extends CreateRecord
|
||||
'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}',
|
||||
]))
|
||||
->helperText('The default startup command that should be used for new servers using this Egg.'),
|
||||
TagsInput::make('features')
|
||||
Forms\Components\TagsInput::make('features')
|
||||
->placeholder('Add Feature')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
Forms\Components\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.
|
||||
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')
|
||||
Forms\Components\Hidden::make('script_is_privileged')
|
||||
->default(1),
|
||||
TagsInput::make('tags')
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
->placeholder('Add Tags')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->url(),
|
||||
KeyValue::make('docker_images')
|
||||
Forms\Components\TextInput::make('update_url')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Forms\Components\KeyValue::make('docker_images')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
@@ -90,37 +76,37 @@ class CreateEgg extends CreateRecord
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
]),
|
||||
|
||||
Tab::make('Process Management')
|
||||
Forms\Components\Tabs\Tab::make('Process Management')
|
||||
->columns()
|
||||
->schema([
|
||||
Hidden::make('config_from')
|
||||
Forms\Components\Hidden::make('config_from')
|
||||
->default(null)
|
||||
->label('Copy Settings From')
|
||||
// ->placeholder('None')
|
||||
// ->relationship('configFrom', 'name', ignoreRecord: true)
|
||||
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
|
||||
TextInput::make('config_stop')
|
||||
Forms\Components\TextInput::make('config_stop')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->label('Stop Command')
|
||||
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
|
||||
->label('Start Configuration')
|
||||
->default('{}')
|
||||
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
Forms\Components\Textarea::make('config_files')->rows(10)->json()
|
||||
->label('Configuration Files')
|
||||
->default('{}')
|
||||
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
|
||||
->label('Log Configuration')
|
||||
->default('{}')
|
||||
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
|
||||
]),
|
||||
Tab::make('Egg Variables')
|
||||
Forms\Components\Tabs\Tab::make('Egg Variables')
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
Forms\Components\Repeater::make('variables')
|
||||
->label('')
|
||||
->addActionLabel('Add New Egg Variable')
|
||||
->grid()
|
||||
@@ -134,7 +120,7 @@ class CreateEgg extends CreateRecord
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['rules'] ??= '';
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -143,76 +129,53 @@ class CreateEgg extends CreateRecord
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['rules'] ??= '';
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
Forms\Components\TextInput::make('name')
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->required(),
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
Forms\Components\Textarea::make('description')->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code')
|
||||
->hintIconTooltip(fn ($state) => "{{{$state}}}")
|
||||
->required(),
|
||||
TextInput::make('default_value')->maxLength(255),
|
||||
Fieldset::make('User Permissions')
|
||||
Forms\Components\TextInput::make('default_value')->maxLength(191),
|
||||
Forms\Components\Fieldset::make('User Permissions')
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
TagsInput::make('rules')
|
||||
->columnSpanFull()
|
||||
->placeholder('Add Rule')
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
'nullable',
|
||||
'string',
|
||||
'integer',
|
||||
'numeric',
|
||||
'boolean',
|
||||
'alpha',
|
||||
'alpha_dash',
|
||||
'alpha_num',
|
||||
'url',
|
||||
'email',
|
||||
'regex:',
|
||||
'min:',
|
||||
'max:',
|
||||
'between:',
|
||||
'between:1024,65535',
|
||||
'in:',
|
||||
'in:true,false',
|
||||
Forms\Components\Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Forms\Components\Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
Forms\Components\Textarea::make('rules')->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
Forms\Components\Tabs\Tab::make('Install Script')
|
||||
->columns(3)
|
||||
->schema([
|
||||
|
||||
Hidden::make('copy_script_from'),
|
||||
Forms\Components\Hidden::make('copy_script_from'),
|
||||
//->placeholder('None')
|
||||
//->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||
|
||||
TextInput::make('script_container')
|
||||
Forms\Components\TextInput::make('script_container')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('ghcr.io/pelican-eggs/installers:debian'),
|
||||
->maxLength(191)
|
||||
->default('alpine:3.4'),
|
||||
|
||||
Select::make('script_entry')
|
||||
Forms\Components\Select::make('script_entry')
|
||||
->selectablePlaceholder(false)
|
||||
->default('bash')
|
||||
->options(['bash', 'ash', '/bin/bash'])
|
||||
|
||||
@@ -2,32 +2,13 @@
|
||||
|
||||
namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Filament\Resources\EggResource;
|
||||
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
|
||||
use App\Models\Egg;
|
||||
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;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
|
||||
class EditEgg extends EditRecord
|
||||
{
|
||||
@@ -37,66 +18,59 @@ class EditEgg extends EditRecord
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('Configuration')
|
||||
Forms\Components\Tabs::make()->tabs([
|
||||
Forms\Components\Tabs\Tab::make('Configuration')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->maxLength(191)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||
TextInput::make('uuid')
|
||||
->label('Egg UUID')
|
||||
Forms\Components\TextInput::make('uuid')
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'),
|
||||
TextInput::make('id')
|
||||
->label('Egg ID')
|
||||
->disabled(),
|
||||
Textarea::make('description')
|
||||
Forms\Components\Textarea::make('description')
|
||||
->rows(3)
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
|
||||
TextInput::make('author')
|
||||
Forms\Components\TextInput::make('author')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->email()
|
||||
->maxLength(191)
|
||||
->disabled()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'),
|
||||
Textarea::make('startup')
|
||||
Forms\Components\Textarea::make('startup')
|
||||
->rows(2)
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->helperText('The default startup command that should be used for new servers using this Egg.'),
|
||||
TagsInput::make('file_denylist')
|
||||
Forms\Components\TagsInput::make('file_denylist')
|
||||
->hidden() // latest wings breaks it.
|
||||
->placeholder('denied-file.txt')
|
||||
->helperText('A list of files that the end user is not allowed to edit.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TagsInput::make('features')
|
||||
Forms\Components\TagsInput::make('features')
|
||||
->placeholder('Add Feature')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
Forms\Components\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.
|
||||
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')
|
||||
Forms\Components\Hidden::make('script_is_privileged')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
TagsInput::make('tags')
|
||||
Forms\Components\TagsInput::make('tags')
|
||||
->placeholder('Add Tags')
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->label('Update URL')
|
||||
->url()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
Forms\Components\TextInput::make('update_url')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
KeyValue::make('docker_images')
|
||||
Forms\Components\KeyValue::make('docker_images')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
@@ -106,32 +80,32 @@ class EditEgg extends EditRecord
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
]),
|
||||
|
||||
Tab::make('Process Management')
|
||||
Forms\Components\Tabs\Tab::make('Process Management')
|
||||
->columns()
|
||||
->schema([
|
||||
Select::make('config_from')
|
||||
Forms\Components\Select::make('config_from')
|
||||
->label('Copy Settings From')
|
||||
->placeholder('None')
|
||||
->relationship('configFrom', 'name', ignoreRecord: true)
|
||||
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
|
||||
TextInput::make('config_stop')
|
||||
->maxLength(255)
|
||||
Forms\Components\TextInput::make('config_stop')
|
||||
->maxLength(191)
|
||||
->label('Stop Command')
|
||||
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
|
||||
Textarea::make('config_startup')->rows(10)->json()
|
||||
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
|
||||
->label('Start Configuration')
|
||||
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
|
||||
Textarea::make('config_files')->rows(10)->json()
|
||||
Forms\Components\Textarea::make('config_files')->rows(10)->json()
|
||||
->label('Configuration Files')
|
||||
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
|
||||
Textarea::make('config_logs')->rows(10)->json()
|
||||
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
|
||||
->label('Log Configuration')
|
||||
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
|
||||
]),
|
||||
Tab::make('Egg Variables')
|
||||
Forms\Components\Tabs\Tab::make('Egg Variables')
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
Forms\Components\Repeater::make('variables')
|
||||
->label('')
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
@@ -144,7 +118,7 @@ class EditEgg extends EditRecord
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['rules'] ??= '';
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -153,78 +127,55 @@ class EditEgg extends EditRecord
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['rules'] ??= '';
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
Forms\Components\TextInput::make('name')
|
||||
->live()
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->required(),
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
TextInput::make('env_variable')
|
||||
Forms\Components\Textarea::make('description')->columnSpanFull(),
|
||||
Forms\Components\TextInput::make('env_variable')
|
||||
->label('Environment Variable')
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->prefix('{{')
|
||||
->suffix('}}')
|
||||
->hintIcon('tabler-code')
|
||||
->hintIconTooltip(fn ($state) => "{{{$state}}}")
|
||||
->required(),
|
||||
TextInput::make('default_value')->maxLength(255),
|
||||
Fieldset::make('User Permissions')
|
||||
Forms\Components\TextInput::make('default_value')->maxLength(191),
|
||||
Forms\Components\Fieldset::make('User Permissions')
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
TagsInput::make('rules')
|
||||
->columnSpanFull()
|
||||
->placeholder('Add Rule')
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
'nullable',
|
||||
'string',
|
||||
'integer',
|
||||
'numeric',
|
||||
'boolean',
|
||||
'alpha',
|
||||
'alpha_dash',
|
||||
'alpha_num',
|
||||
'url',
|
||||
'email',
|
||||
'regex:',
|
||||
'min:',
|
||||
'max:',
|
||||
'between:',
|
||||
'between:1024,65535',
|
||||
'in:',
|
||||
'in:true,false',
|
||||
Forms\Components\Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Forms\Components\Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
Forms\Components\TextInput::make('rules')->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
Forms\Components\Tabs\Tab::make('Install Script')
|
||||
->columns(3)
|
||||
->schema([
|
||||
|
||||
Select::make('copy_script_from')
|
||||
Forms\Components\Select::make('copy_script_from')
|
||||
->placeholder('None')
|
||||
->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||
|
||||
TextInput::make('script_container')
|
||||
Forms\Components\TextInput::make('script_container')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->default('alpine:3.4'),
|
||||
|
||||
TextInput::make('script_entry')
|
||||
Forms\Components\TextInput::make('script_entry')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->default('ash'),
|
||||
|
||||
MonacoEditor::make('script_install')
|
||||
@@ -242,104 +193,15 @@ class EditEgg extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make('deleteEgg')
|
||||
Actions\DeleteAction::make()
|
||||
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
|
||||
Actions\Action::make('exportEgg')
|
||||
->label('Export')
|
||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete Egg' : 'Egg In Use'),
|
||||
Actions\ExportAction::make()
|
||||
->icon('tabler-download')
|
||||
->label('Export Egg')
|
||||
->color('primary')
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->authorize(fn () => auth()->user()->can('export egg')),
|
||||
Actions\Action::make('importEgg')
|
||||
->label('Import')
|
||||
->form([
|
||||
Placeholder::make('warning')
|
||||
->label('This will overwrite the current egg to the one you upload.'),
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tab::make('From File')
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('egg')
|
||||
->label('Egg')
|
||||
->hint('eg. minecraft.json')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false),
|
||||
]),
|
||||
Tab::make('From URL')
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->default(fn (Egg $egg): ?string => $egg->update_url)
|
||||
->hint('Link to the egg file (eg. minecraft.json)')
|
||||
->url(),
|
||||
]),
|
||||
])
|
||||
->contained(false),
|
||||
|
||||
])
|
||||
->action(function (array $data, Egg $egg): void {
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
if (!empty($data['egg'])) {
|
||||
try {
|
||||
$eggImportService->fromFile($data['egg'], $egg);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger() // Will Robinson
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
} elseif (!empty($data['url'])) {
|
||||
try {
|
||||
$eggImportService->fromUrl($data['url'], $egg);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->refreshForm();
|
||||
Notification::make()
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
public function refreshForm(): void
|
||||
{
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
ServersRelationManager::class,
|
||||
// TODO uses old admin panel export service
|
||||
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,15 @@ namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use App\Filament\Resources\EggResource;
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListEggs extends ListRecords
|
||||
{
|
||||
@@ -29,39 +21,54 @@ class ListEggs extends ListRecords
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->searchable(true)
|
||||
->searchable(false)
|
||||
->defaultPaginationPageOption(25)
|
||||
->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0)
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
->label('Id')
|
||||
->hidden(),
|
||||
TextColumn::make('name')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->icon('tabler-egg')
|
||||
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
|
||||
->description(fn ($record): ?string => $record->description)
|
||||
->wrap()
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('servers_count')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('author')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('servers_count')
|
||||
->counts('servers')
|
||||
->icon('tabler-server')
|
||||
->label('Servers'),
|
||||
Tables\Columns\TextColumn::make('script_container')
|
||||
->searchable()
|
||||
->hidden(),
|
||||
Tables\Columns\TextColumn::make('copyFrom.name')
|
||||
->hidden()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('script_entry')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Tables\Actions\Action::make('export')
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\ExportAction::make()
|
||||
->icon('tabler-download')
|
||||
->label('Export')
|
||||
->color('primary')
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->authorize(fn () => auth()->user()->can('export egg')),
|
||||
// TODO uses old admin panel export service
|
||||
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg])),
|
||||
])
|
||||
->headerActions([
|
||||
//
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete egg')),
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -73,57 +80,21 @@ class ListEggs extends ListRecords
|
||||
Actions\Action::make('import')
|
||||
->label('Import')
|
||||
->form([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tab::make('From File')
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('egg')
|
||||
->label('Egg')
|
||||
->hint('This should be the json file ( egg-minecraft.json )')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false)
|
||||
->multiple(),
|
||||
]),
|
||||
Tab::make('From URL')
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->hint('This URL should point to a single json file')
|
||||
->url(),
|
||||
]),
|
||||
])
|
||||
->contained(false),
|
||||
|
||||
Forms\Components\FileUpload::make('egg')
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->storeFiles(false)
|
||||
->multiple(),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
/** @var TemporaryUploadedFile $eggFile */
|
||||
$eggFile = $data['egg'];
|
||||
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
if (!empty($data['egg'])) {
|
||||
/** @var TemporaryUploadedFile[] $eggFile */
|
||||
$eggFile = $data['egg'];
|
||||
|
||||
foreach ($eggFile as $file) {
|
||||
try {
|
||||
$eggImportService->fromFile($file);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['url'])) {
|
||||
foreach ($eggFile as $file) {
|
||||
try {
|
||||
$eggImportService->fromUrl($data['url']);
|
||||
$eggImportService->handle($file);
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
@@ -140,8 +111,7 @@ class ListEggs extends ListRecords
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
class ServersRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'servers';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
|
||||
->searchable(false)
|
||||
->columns([
|
||||
TextColumn::make('user.username')
|
||||
->label('Owner')
|
||||
->icon('tabler-user')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
TextColumn::make('name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||
->sortable(),
|
||||
TextColumn::make('node.name')
|
||||
->icon('tabler-server-2')
|
||||
->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,12 +11,6 @@ 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
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
namespace App\Filament\Resources\MountResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MountResource;
|
||||
use App\Services\Servers\ServerCreationService;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
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\CreateRecord;
|
||||
use Filament\Forms;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -26,11 +24,11 @@ class CreateMount extends CreateRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
TextInput::make('name')
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->helperText('Unique name used to separate this mount from another.')
|
||||
->maxLength(64),
|
||||
ToggleButtons::make('read_only')
|
||||
Forms\Components\ToggleButtons::make('read_only')
|
||||
->label('Read only?')
|
||||
->helperText('Is the mount read only inside the container?')
|
||||
->options([
|
||||
@@ -48,15 +46,15 @@ class CreateMount extends CreateRecord
|
||||
->inline()
|
||||
->default(false)
|
||||
->required(),
|
||||
TextInput::make('source')
|
||||
Forms\Components\TextInput::make('source')
|
||||
->required()
|
||||
->helperText('File path on the host system to mount to a container.')
|
||||
->maxLength(255),
|
||||
TextInput::make('target')
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('target')
|
||||
->required()
|
||||
->helperText('Where the mount will be accessible inside a container.')
|
||||
->maxLength(255),
|
||||
ToggleButtons::make('user_mountable')
|
||||
->maxLength(191),
|
||||
Forms\Components\ToggleButtons::make('user_mountable')
|
||||
->hidden()
|
||||
->label('User mountable?')
|
||||
->options([
|
||||
@@ -74,10 +72,10 @@ class CreateMount extends CreateRecord
|
||||
->default(false)
|
||||
->inline()
|
||||
->required(),
|
||||
Textarea::make('description')
|
||||
Forms\Components\Textarea::make('description')
|
||||
->helperText('A longer description for this mount.')
|
||||
->columnSpanFull(),
|
||||
Hidden::make('user_mountable')->default(1),
|
||||
Forms\Components\Hidden::make('user_mountable')->default(1),
|
||||
])->columnSpan(1)->columns([
|
||||
'default' => 1,
|
||||
'lg' => 2,
|
||||
|
||||
@@ -4,10 +4,8 @@ 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;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
@@ -22,11 +20,11 @@ class EditMount extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
TextInput::make('name')
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->helperText('Unique name used to separate this mount from another.')
|
||||
->maxLength(64),
|
||||
ToggleButtons::make('read_only')
|
||||
Forms\Components\ToggleButtons::make('read_only')
|
||||
->label('Read only?')
|
||||
->helperText('Is the mount read only inside the container?')
|
||||
->options([
|
||||
@@ -44,15 +42,15 @@ class EditMount extends EditRecord
|
||||
->inline()
|
||||
->default(false)
|
||||
->required(),
|
||||
TextInput::make('source')
|
||||
Forms\Components\TextInput::make('source')
|
||||
->required()
|
||||
->helperText('File path on the host system to mount to a container.')
|
||||
->maxLength(255),
|
||||
TextInput::make('target')
|
||||
->maxLength(191),
|
||||
Forms\Components\TextInput::make('target')
|
||||
->required()
|
||||
->helperText('Where the mount will be accessible inside a container.')
|
||||
->maxLength(255),
|
||||
ToggleButtons::make('user_mountable')
|
||||
->maxLength(191),
|
||||
Forms\Components\ToggleButtons::make('user_mountable')
|
||||
->hidden()
|
||||
->label('User mountable?')
|
||||
->options([
|
||||
@@ -70,7 +68,7 @@ class EditMount extends EditRecord
|
||||
->default(false)
|
||||
->inline()
|
||||
->required(),
|
||||
Textarea::make('description')
|
||||
Forms\Components\Textarea::make('description')
|
||||
->helperText('A longer description for this mount.')
|
||||
->columnSpanFull(),
|
||||
])->columnSpan(1)->columns([
|
||||
@@ -100,12 +98,6 @@ class EditMount extends EditRecord
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,9 @@ use App\Filament\Resources\MountResource;
|
||||
use App\Models\Mount;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListMounts extends ListRecords
|
||||
{
|
||||
@@ -22,29 +18,31 @@ class ListMounts extends ListRecords
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('source')
|
||||
Tables\Columns\TextColumn::make('source')
|
||||
->searchable(),
|
||||
TextColumn::make('target')
|
||||
Tables\Columns\TextColumn::make('target')
|
||||
->searchable(),
|
||||
IconColumn::make('read_only')
|
||||
Tables\Columns\IconColumn::make('read_only')
|
||||
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
|
||||
->color(fn (bool $state) => $state ? 'success' : 'danger')
|
||||
->sortable(),
|
||||
IconColumn::make('user_mountable')
|
||||
Tables\Columns\IconColumn::make('user_mountable')
|
||||
->hidden()
|
||||
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
|
||||
->color(fn (bool $state) => $state ? 'success' : 'danger')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete mount')),
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-layers-linked')
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
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\Filament\Resources\NodeResource\RelationManagers;
|
||||
use App\Models\Node;
|
||||
use Filament\Resources\Resource;
|
||||
|
||||
@@ -16,16 +15,11 @@ class NodeResource extends Resource
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
AllocationsRelationManager::class,
|
||||
NodesRelationManager::class,
|
||||
RelationManagers\AllocationsRelationManager::class,
|
||||
RelationManagers\NodesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,8 @@
|
||||
namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class CreateNode extends CreateRecord
|
||||
@@ -28,375 +18,177 @@ class CreateNode extends CreateRecord
|
||||
public function form(Forms\Form $form): Forms\Form
|
||||
{
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Wizard::make([
|
||||
Step::make('basic')
|
||||
->label('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return '
|
||||
Forms\Components\TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
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
|
||||
';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return "
|
||||
return "
|
||||
This is the domain name that points to your node's IP Address.
|
||||
If you've already set up this, you can verify it by checking the next field!
|
||||
";
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return 'You cannot connect to an IP Address over SSL';
|
||||
}
|
||||
";
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return 'You cannot connect to an IP Address over SSL';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', collect($validRecords)->first());
|
||||
$set('ip', collect($validRecords)->first());
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(255),
|
||||
$set('dns', false);
|
||||
})
|
||||
->maxLength(191),
|
||||
|
||||
TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
Forms\Components\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.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
false => 'Invalid',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Forms\Components\ToggleButtons::make('dns')
|
||||
->label('DNS Record Check')
|
||||
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
false => 'Invalid',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
Forms\Components\TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(0)
|
||||
->maxValue(65536)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
Forms\Components\TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
Forms\Components\ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->required()
|
||||
->inline()
|
||||
->helperText(function (Forms\Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return 'An IP address cannot use SSL.';
|
||||
}
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return 'An IP address cannot use SSL.';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
]),
|
||||
Step::make('advanced')
|
||||
->label('Advanced Settings')
|
||||
->icon('tabler-server-cog')
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
ToggleButtons::make('maintenance_mode')
|
||||
->label('Maintenance Mode')->inline()
|
||||
->columnSpan(1)
|
||||
->default(false)
|
||||
->hinticon('tabler-question-mark')
|
||||
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||
->options([
|
||||
true => 'Enable',
|
||||
false => 'Disable',
|
||||
])
|
||||
->colors([
|
||||
true => 'danger',
|
||||
false => 'success',
|
||||
]),
|
||||
ToggleButtons::make('public')
|
||||
->default(true)
|
||||
->columnSpan(1)
|
||||
->label('Automatic Allocation')->inline()
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
TagsInput::make('tags')
|
||||
->label('Tags')
|
||||
->disabled()
|
||||
->placeholder('Not Implemented')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Not Implemented')
|
||||
->columnSpan(2),
|
||||
TextInput::make('upload_size')
|
||||
->label('Upload Limit')
|
||||
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->columnSpan(1)
|
||||
->numeric()->required()
|
||||
->default(256)
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||
TextInput::make('daemon_sftp')
|
||||
->columnSpan(1)
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
TextInput::make('daemon_sftp_alias')
|
||||
->columnSpan(2)
|
||||
->label('SFTP Alias')
|
||||
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0)
|
||||
->required(),
|
||||
TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->default(0)
|
||||
->required(),
|
||||
TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->default(0)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_cpu')
|
||||
->label('CPU')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('CPU Limit')->inlineLabel()
|
||||
->suffix('%')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(0)
|
||||
->minValue(0)
|
||||
->required(),
|
||||
TextInput::make('cpu_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->default(0)
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%')
|
||||
->required(),
|
||||
]),
|
||||
]),
|
||||
])->columnSpanFull()
|
||||
->nextAction(fn (Action $action) => $action->label('Next Step'))
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
size="sm"
|
||||
>
|
||||
Create Node
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
->label('strings.description')
|
||||
->hidden()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->rows(5),
|
||||
|
||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -406,9 +198,4 @@ class CreateNode extends CreateRecord
|
||||
'tab' => '-configuration-tab',
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,12 @@
|
||||
namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart;
|
||||
use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart;
|
||||
use App\Models\Node;
|
||||
use App\Services\Nodes\NodeUpdateService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\View;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
@@ -41,400 +30,28 @@ class EditNode extends EditRecord
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tab::make('')
|
||||
->label('Overview')
|
||||
->icon('tabler-chart-area-line-filled')
|
||||
->columns(6)
|
||||
->schema([
|
||||
Fieldset::make()
|
||||
->label('Node Information')
|
||||
->columns(4)
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->label('Wings Version')
|
||||
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
|
||||
Placeholder::make('')
|
||||
->label('CPU Threads')
|
||||
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
|
||||
Placeholder::make('')
|
||||
->label('Architecture')
|
||||
->content(fn (Node $node) => $node->systemInformation()['architecture'] ?? 'Unknown'),
|
||||
Placeholder::make('')
|
||||
->label('Kernel')
|
||||
->content(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? 'Unknown'),
|
||||
]),
|
||||
View::make('filament.components.node-cpu-chart')->columnSpan(3),
|
||||
View::make('filament.components.node-memory-chart')->columnSpan(3),
|
||||
// TODO: Make purdy View::make('filament.components.node-storage-chart')->columnSpan(3),
|
||||
]),
|
||||
Tab::make('Basic Settings')
|
||||
Tabs\Tab::make('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->schema([
|
||||
TextInput::make('fqdn')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
if (is_ip($state)) {
|
||||
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
|
||||
';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return "
|
||||
This is the domain name that points to your node's IP Address.
|
||||
If you've already set up this, you can verify it by checking the next field!
|
||||
";
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return 'You cannot connect to an IP Address over SSL';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->afterStateUpdated(function (Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$validRecords = gethostbynamel($state);
|
||||
if ($validRecords) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', collect($validRecords)->first());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$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.')
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => 'Valid',
|
||||
false => 'Invalid',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
TextInput::make('name')
|
||||
->label('Display Name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->required()
|
||||
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||
->helperText('This name is for display only and can be changed later.')
|
||||
->maxLength(100),
|
||||
|
||||
ToggleButtons::make('scheme')
|
||||
->label('Communicate over SSL')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return 'An IP address cannot use SSL.';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
'http' => 'HTTP',
|
||||
'https' => 'HTTPS (SSL)',
|
||||
])
|
||||
->colors([
|
||||
'http' => 'warning',
|
||||
'https' => 'success',
|
||||
])
|
||||
->icons([
|
||||
'http' => 'tabler-lock-open-off',
|
||||
'https' => 'tabler-lock',
|
||||
])
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||
Tab::make('Advanced Settings')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
TextInput::make('id')
|
||||
->label('Node ID')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->disabled(),
|
||||
TextInput::make('uuid')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->label('Node UUID')
|
||||
->hintAction(CopyAction::make())
|
||||
->disabled(),
|
||||
TagsInput::make('tags')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->label('Tags')
|
||||
->disabled()
|
||||
->placeholder('Not Implemented')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Not Implemented'),
|
||||
TextInput::make('upload_size')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||
->label('Upload Limit')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||
->numeric()->required()
|
||||
->minValue(1)
|
||||
->maxValue(1024)
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
|
||||
TextInput::make('daemon_sftp')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
TextInput::make('daemon_sftp_alias')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('SFTP Alias')
|
||||
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
|
||||
ToggleButtons::make('public')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('Automatic Allocation')->inline()
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
]),
|
||||
ToggleButtons::make('maintenance_mode')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('Maintenance Mode')->inline()
|
||||
->hinticon('tabler-question-mark')
|
||||
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||
->options([
|
||||
false => 'Disable',
|
||||
true => 'Enable',
|
||||
])
|
||||
->colors([
|
||||
false => 'success',
|
||||
true => 'danger',
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_mem')
|
||||
->label('Memory')->inlineLabel()->inline()
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
|
||||
->live()
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
||||
TextInput::make('memory')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->label('Memory Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->required()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
TextInput::make('memory_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->required()
|
||||
->hidden(fn (Get $get) => $get('unlimited_mem'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_disk')
|
||||
->label('Disk')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
||||
TextInput::make('disk')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Disk Limit')->inlineLabel()
|
||||
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
|
||||
->required()
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
TextInput::make('disk_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_disk'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(6)
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
ToggleButtons::make('unlimited_cpu')
|
||||
->label('CPU')->inlineLabel()->inline()
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
|
||||
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
|
||||
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
|
||||
->options([
|
||||
true => 'Unlimited',
|
||||
false => 'Limited',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'warning',
|
||||
])
|
||||
->columnSpan(2),
|
||||
TextInput::make('cpu')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('CPU Limit')->inlineLabel()
|
||||
->suffix('%')
|
||||
->required()
|
||||
->columnSpan(2)
|
||||
->numeric()
|
||||
->minValue(0),
|
||||
TextInput::make('cpu_overallocate')
|
||||
->dehydratedWhenHidden()
|
||||
->hidden(fn (Get $get) => $get('unlimited_cpu'))
|
||||
->label('Overallocate')->inlineLabel()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(-1)
|
||||
->maxValue(100)
|
||||
->suffix('%'),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Configuration File')
|
||||
->schema((new CreateNode())->form($form)->getComponents()),
|
||||
// Tabs\Tab::make('Advanced Settings')
|
||||
// ->icon('tabler-server-cog')
|
||||
// ->schema([
|
||||
// Forms\Components\Placeholder::make('Coming soon!'),
|
||||
// ]),
|
||||
Tabs\Tab::make('Configuration')
|
||||
->icon('tabler-code')
|
||||
->schema([
|
||||
Placeholder::make('instructions')
|
||||
Forms\Components\Placeholder::make('instructions')
|
||||
->columnSpanFull()
|
||||
->content(new HtmlString('
|
||||
Save this file to your <span title="usually /etc/pelican/">daemon\'s root directory</span>, named <code>config.yml</code>
|
||||
')),
|
||||
Textarea::make('config')
|
||||
Forms\Components\Textarea::make('config')
|
||||
->label('/etc/pelican/config.yml')
|
||||
->disabled()
|
||||
->rows(19)
|
||||
->hintAction(CopyAction::make())
|
||||
->columnSpanFull(),
|
||||
Forms\Components\Actions::make([
|
||||
Forms\Components\Actions\Action::make('resetKey')
|
||||
->label('Reset Daemon Token')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Reset Daemon Token?')
|
||||
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
|
||||
->action(function (NodeUpdateService $nodeUpdateService, Node $node) {
|
||||
$nodeUpdateService->handle($node, [], true);
|
||||
Notification::make()->success()->title('Daemon Key Reset')->send();
|
||||
$this->fillForm();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
@@ -449,31 +66,26 @@ class EditNode extends EditRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
protected function getSteps(): array
|
||||
{
|
||||
return [];
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->disabled(fn (Node $node) => $node->servers()->count() > 0)
|
||||
->label(fn (Node $node) => $node->servers()->count() > 0 ? 'Node Has Servers' : 'Delete'),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
protected function getFooterWidgets(): array
|
||||
{
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
protected function getColumnSpan()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
protected function getColumnStart()
|
||||
{
|
||||
return null;
|
||||
return [
|
||||
NodeStorageChart::class,
|
||||
NodeMemoryChart::class,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,9 @@ use App\Filament\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListNodes extends ListRecords
|
||||
{
|
||||
@@ -24,68 +20,64 @@ class ListNodes extends ListRecords
|
||||
->searchable(false)
|
||||
->checkIfRecordIsSelectableUsing(fn (Node $node) => $node->servers_count <= 0)
|
||||
->columns([
|
||||
TextColumn::make('uuid')
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
->label('UUID')
|
||||
->searchable()
|
||||
->hidden(),
|
||||
IconColumn::make('health')
|
||||
Tables\Columns\IconColumn::make('health')
|
||||
->alignCenter()
|
||||
->state(fn (Node $node) => $node)
|
||||
->view('livewire.columns.version-column'),
|
||||
TextColumn::make('name')
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->icon('tabler-server-2')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('fqdn')
|
||||
Tables\Columns\TextColumn::make('fqdn')
|
||||
->visibleFrom('md')
|
||||
->label('Address')
|
||||
->icon('tabler-network')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('memory')
|
||||
Tables\Columns\TextColumn::make('memory')
|
||||
->visibleFrom('sm')
|
||||
->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))
|
||||
->suffix(' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
|
||||
->sortable(),
|
||||
TextColumn::make('disk')
|
||||
Tables\Columns\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))
|
||||
->suffix(' GB')
|
||||
->formatStateUsing(fn ($state) => number_format($state / 1000, 2))
|
||||
->sortable(),
|
||||
TextColumn::make('cpu')
|
||||
->visibleFrom('sm')
|
||||
->icon('tabler-file')
|
||||
->numeric()
|
||||
->suffix(' %')
|
||||
->sortable(),
|
||||
IconColumn::make('scheme')
|
||||
Tables\Columns\IconColumn::make('scheme')
|
||||
->visibleFrom('xl')
|
||||
->label('SSL')
|
||||
->trueIcon('tabler-lock')
|
||||
->falseIcon('tabler-lock-open-off')
|
||||
->state(fn (Node $node) => $node->scheme === 'https'),
|
||||
IconColumn::make('public')
|
||||
Tables\Columns\IconColumn::make('public')
|
||||
->visibleFrom('lg')
|
||||
->trueIcon('tabler-eye-check')
|
||||
->falseIcon('tabler-eye-cancel'),
|
||||
TextColumn::make('servers_count')
|
||||
Tables\Columns\TextColumn::make('servers_count')
|
||||
->visibleFrom('sm')
|
||||
->counts('servers')
|
||||
->label('Servers')
|
||||
->sortable()
|
||||
->icon('tabler-brand-docker'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete node')),
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-server-2')
|
||||
|
||||
@@ -3,24 +3,15 @@
|
||||
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms;
|
||||
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';
|
||||
@@ -31,7 +22,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
Forms\Components\TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
@@ -49,21 +40,17 @@ class AllocationsRelationManager extends RelationManager
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||
->searchable()
|
||||
->columns([
|
||||
TextColumn::make('id'),
|
||||
TextColumn::make('port')
|
||||
->searchable()
|
||||
->label('Port'),
|
||||
TextColumn::make('server.name')
|
||||
Tables\Columns\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()
|
||||
Tables\Columns\TextColumn::make('ip_alias')
|
||||
->label('Alias'),
|
||||
TextInputColumn::make('ip')
|
||||
->searchable()
|
||||
Tables\Columns\TextColumn::make('ip')
|
||||
->label('IP'),
|
||||
Tables\Columns\TextColumn::make('port')
|
||||
->searchable()
|
||||
->label('Port'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
@@ -74,20 +61,20 @@ class AllocationsRelationManager extends RelationManager
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
|
||||
->form(fn () => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->ipAddresses())
|
||||
Forms\Components\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')
|
||||
Forms\Components\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')
|
||||
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.
|
||||
@@ -97,7 +84,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
@@ -122,7 +109,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
foreach (range($start, $end) as $i) {
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
@@ -139,8 +126,6 @@ class AllocationsRelationManager extends RelationManager
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
@@ -151,9 +136,9 @@ class AllocationsRelationManager extends RelationManager
|
||||
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord(), $data)),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete allocation')),
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
// Tables\Actions\DissociateBulkAction::make(),
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
|
||||
@@ -19,34 +18,34 @@ class NodesRelationManager extends RelationManager
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
TextColumn::make('user.username')
|
||||
Tables\Columns\TextColumn::make('user.username')
|
||||
->label('Owner')
|
||||
->icon('tabler-user')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->searchable(),
|
||||
TextColumn::make('name')
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('egg.name')
|
||||
Tables\Columns\TextColumn::make('egg.name')
|
||||
->icon('tabler-egg')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
SelectColumn::make('allocation.id')
|
||||
Tables\Columns\SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->options(fn ($state, 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')
|
||||
Tables\Columns\TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
|
||||
Tables\Columns\TextColumn::make('cpu')->icon('tabler-cpu'),
|
||||
Tables\Columns\TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
->label('Databases')
|
||||
->icon('tabler-database')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('backups_count')
|
||||
Tables\Columns\TextColumn::make('backups_count')
|
||||
->counts('backups')
|
||||
->label('Backups')
|
||||
->icon('tabler-file-download')
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NodeCpuChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '5s';
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
$threads = $node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$cpu = collect(cache()->get("nodes.$node->id.cpu_percent"))
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => number_format($value * $threads, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($cpu, 'cpu'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($cpu, 'timestamp'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
/** @var Node $node */
|
||||
$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) . '%';
|
||||
|
||||
return 'CPU - ' . $cpu . '% Of ' . $max;
|
||||
}
|
||||
}
|
||||
@@ -3,83 +3,66 @@
|
||||
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NodeMemoryChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '5s';
|
||||
protected static ?string $maxHeight = '300px';
|
||||
protected static ?string $heading = 'Memory';
|
||||
|
||||
protected static ?string $pollingInterval = '60s';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
protected static ?array $options = [
|
||||
'scales' => [
|
||||
'x' => [
|
||||
'grid' => [
|
||||
'display' => false,
|
||||
],
|
||||
'ticks' => [
|
||||
'display' => false,
|
||||
],
|
||||
],
|
||||
'y' => [
|
||||
'grid' => [
|
||||
'display' => false,
|
||||
],
|
||||
'ticks' => [
|
||||
'display' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$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,
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
$total = $node->statistics()['memory_total'] ?? 0;
|
||||
$used = $node->statistics()['memory_used'] ?? 0;
|
||||
$unused = $total - $used;
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($memUsed, 'memory'),
|
||||
'label' => 'Data Cool',
|
||||
'data' => [$used, $unused],
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
// 'backgroundColor' => [],
|
||||
],
|
||||
'labels' => array_column($memUsed, 'timestamp'),
|
||||
'labels' => ['Used', 'Unused'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
$latestMemoryUsed = collect(cache()->get("nodes.$node->id.memory_used"))->last();
|
||||
$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';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? number_format($totalMemory / 1024 / 1024 / 1024, 2) .' GiB'
|
||||
: number_format($totalMemory / 1000 / 1000 / 1000, 2) . ' GB';
|
||||
|
||||
return 'Memory - ' . $used . ' Of ' . $total;
|
||||
return 'pie';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ 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;
|
||||
|
||||
@@ -40,13 +40,14 @@ class NodeStorageChart extends ChartWidget
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$total = ($node->statistics()['disk_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$used = ($node->statistics()['disk_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$total = $node->statistics()['disk_total'] ?? 0;
|
||||
$used = $node->statistics()['disk_used'] ?? 0;
|
||||
$unused = $total - $used;
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Data Cool',
|
||||
'data' => [$used, $unused],
|
||||
'backgroundColor' => [
|
||||
'rgb(255, 99, 132)',
|
||||
@@ -54,6 +55,7 @@ class NodeStorageChart extends ChartWidget
|
||||
'rgb(255, 205, 86)',
|
||||
],
|
||||
],
|
||||
// 'backgroundColor' => [],
|
||||
],
|
||||
'labels' => ['Used', 'Unused'],
|
||||
];
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\RolePermissionModels;
|
||||
use App\Enums\RolePermissionPrefixes;
|
||||
use App\Filament\Resources\RoleResource\Pages;
|
||||
use App\Models\Role;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RoleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Role::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-users-group';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
$permissions = [];
|
||||
|
||||
foreach (RolePermissionModels::cases() as $model) {
|
||||
$options = [];
|
||||
|
||||
foreach (RolePermissionPrefixes::cases() as $prefix) {
|
||||
$options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix->value);
|
||||
}
|
||||
|
||||
if (array_key_exists($model->value, Role::MODEL_SPECIFIC_PERMISSIONS)) {
|
||||
foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model->value] as $permission) {
|
||||
$options[$permission . ' ' . strtolower($model->value)] = Str::headline($permission);
|
||||
}
|
||||
}
|
||||
|
||||
$permissions[] = self::makeSection($model->value, $options);
|
||||
}
|
||||
|
||||
foreach (Role::SPECIAL_PERMISSIONS as $model => $prefixes) {
|
||||
$options = [];
|
||||
|
||||
foreach ($prefixes as $prefix) {
|
||||
$options[$prefix . ' ' . strtolower($model)] = Str::headline($prefix);
|
||||
}
|
||||
|
||||
$permissions[] = self::makeSection($model, $options);
|
||||
}
|
||||
|
||||
return $form
|
||||
->columns(1)
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Role Name')
|
||||
->required()
|
||||
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
TextInput::make('guard_name')
|
||||
->label('Guard Name')
|
||||
->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '')
|
||||
->nullable()
|
||||
->hidden(),
|
||||
Fieldset::make('Permissions')
|
||||
->columns(3)
|
||||
->schema($permissions)
|
||||
->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
Placeholder::make('permissions')
|
||||
->content('The Root Admin has all permissions.')
|
||||
->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function makeSection(string $model, array $options): Section
|
||||
{
|
||||
$icon = null;
|
||||
|
||||
if (class_exists('\App\Filament\Resources\\' . $model . 'Resource')) {
|
||||
$icon = ('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon();
|
||||
} elseif (class_exists('\App\Filament\Pages\\' . $model)) {
|
||||
$icon = ('\App\Filament\Pages\\' . $model)::getNavigationIcon();
|
||||
}
|
||||
|
||||
return Section::make(Str::headline(Str::plural($model)))
|
||||
->columnSpan(1)
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->icon($icon)
|
||||
->headerActions([
|
||||
Action::make('count')
|
||||
->label(fn (Get $get) => count($get(strtolower($model) . '_list')))
|
||||
->badge(),
|
||||
])
|
||||
->schema([
|
||||
CheckboxList::make(strtolower($model) . '_list')
|
||||
->label('')
|
||||
->options($options)
|
||||
->columns()
|
||||
->gridDirection('row')
|
||||
->bulkToggleable()
|
||||
->live()
|
||||
->afterStateHydrated(
|
||||
function (Component $component, string $operation, ?Role $record) use ($options) {
|
||||
if (in_array($operation, ['edit', 'view'])) {
|
||||
|
||||
if (blank($record)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($component->isVisible()) {
|
||||
$component->state(
|
||||
collect($options)
|
||||
->filter(fn ($value, $key) => $record->checkPermissionTo($key))
|
||||
->keys()
|
||||
->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
->dehydrated(fn ($state) => !blank($state)),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListRoles::route('/'),
|
||||
'create' => Pages\CreateRole::route('/create'),
|
||||
'edit' => Pages\EditRole::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
/**
|
||||
* @property Role $record
|
||||
*/
|
||||
class CreateRole extends CreateRecord
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public Collection $permissions;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->permissions = collect($data)
|
||||
->filter(function ($permission, $key) {
|
||||
return !in_array($key, ['name', 'guard_name']);
|
||||
})
|
||||
->values()
|
||||
->flatten()
|
||||
->unique();
|
||||
|
||||
return Arr::only($data, ['name', 'guard_name']);
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$permissionModels = collect();
|
||||
$this->permissions->each(function ($permission) use ($permissionModels) {
|
||||
$permissionModels->push(Permission::firstOrCreate([
|
||||
'name' => $permission,
|
||||
'guard_name' => $this->data['guard_name'],
|
||||
]));
|
||||
});
|
||||
|
||||
$this->record->syncPermissions($permissionModels);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
/**
|
||||
* @property Role $record
|
||||
*/
|
||||
class EditRole extends EditRecord
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
public Collection $permissions;
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->permissions = collect($data)
|
||||
->filter(function ($permission, $key) {
|
||||
return !in_array($key, ['name', 'guard_name']);
|
||||
})
|
||||
->values()
|
||||
->flatten()
|
||||
->unique();
|
||||
|
||||
return Arr::only($data, ['name', 'guard_name']);
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$permissionModels = collect();
|
||||
$this->permissions->each(function ($permission) use ($permissionModels) {
|
||||
$permissionModels->push(Permission::firstOrCreate([
|
||||
'name' => $permission,
|
||||
'guard_name' => $this->data['guard_name'],
|
||||
]));
|
||||
});
|
||||
|
||||
$this->record->syncPermissions($permissionModels);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1)
|
||||
->label(fn (Role $role) => $role->isRootAdmin() ? 'Can\'t delete Root Admin' : ($role->users_count >= 1 ? 'In Use' : 'Delete')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction as CreateActionTable;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListRoles extends ListRecords
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('guard_name')
|
||||
->hidden()
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('permissions_count')
|
||||
->label('Permissions')
|
||||
->badge()
|
||||
->counts('permissions')
|
||||
->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? 'All' : $state),
|
||||
TextColumn::make('users_count')
|
||||
->label('Users')
|
||||
->counts('users')
|
||||
->icon('tabler-users'),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0)
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete role')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-users-group')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Roles')
|
||||
->emptyStateActions([
|
||||
CreateActionTable::make('create')
|
||||
->label('Create Role')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Create Role'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,6 @@ class ServerResource extends Resource
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,9 @@ 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\CreateAction;
|
||||
use Filament\Tables\Grouping\Group;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
@@ -20,18 +18,35 @@ class ListServers extends ListRecords
|
||||
{
|
||||
return $table
|
||||
->searchable(false)
|
||||
->defaultGroup('node.name')
|
||||
->groups([
|
||||
Group::make('node.name')->getDescriptionFromRecordUsing(fn (Server $server): string => str($server->node->description)->limit(150)),
|
||||
Group::make('user.username')->getDescriptionFromRecordUsing(fn (Server $server): string => $server->user->email),
|
||||
Group::make('egg.name')->getDescriptionFromRecordUsing(fn (Server $server): string => str($server->egg->description)->limit(150)),
|
||||
])
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('condition')
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->default('unknown')
|
||||
->badge()
|
||||
->icon(fn (Server $server) => $server->conditionIcon())
|
||||
->color(fn (Server $server) => $server->conditionColor()),
|
||||
->default(function (Server $server) {
|
||||
if ($server->status !== null) {
|
||||
return $server->status;
|
||||
}
|
||||
|
||||
return $server->retrieveStatus() ?? 'node_fail';
|
||||
})
|
||||
->icon(fn ($state) => match ($state) {
|
||||
'node_fail' => 'tabler-server-off',
|
||||
'running' => 'tabler-heartbeat',
|
||||
'removing' => 'tabler-heart-x',
|
||||
'offline' => 'tabler-heart-off',
|
||||
'paused' => 'tabler-heart-pause',
|
||||
'installing' => 'tabler-heart-bolt',
|
||||
'suspended' => 'tabler-heart-cancel',
|
||||
default => 'tabler-heart-question',
|
||||
})
|
||||
->color(fn ($state): string => match ($state) {
|
||||
'running' => 'success',
|
||||
'installing', 'restarting' => 'primary',
|
||||
'paused', 'removing' => 'warning',
|
||||
'node_fail', 'install_failed', 'suspended' => 'danger',
|
||||
default => 'gray',
|
||||
}),
|
||||
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
->hidden()
|
||||
->label('UUID')
|
||||
@@ -43,25 +58,19 @@ class ListServers extends ListRecords
|
||||
Tables\Columns\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(),
|
||||
->sortable(),
|
||||
Tables\Columns\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(),
|
||||
->sortable(),
|
||||
Tables\Columns\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(),
|
||||
->sortable(),
|
||||
Tables\Columns\SelectColumn::make('allocation_id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(
|
||||
->options(fn ($state, Server $server) => $server->allocations->mapWithKeys(
|
||||
fn ($allocation) => [$allocation->id => $allocation->address])
|
||||
)
|
||||
->selectablePlaceholder(false)
|
||||
@@ -74,20 +83,16 @@ class ListServers extends ListRecords
|
||||
->numeric()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\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;
|
||||
}),
|
||||
->url(fn (Server $server) => "/server/$server->uuid_short"),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
->searchable()
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Servers')
|
||||
->emptyStateActions([
|
||||
|
||||
@@ -3,20 +3,12 @@
|
||||
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;
|
||||
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';
|
||||
@@ -25,7 +17,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
Forms\Components\TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
@@ -35,23 +27,21 @@ class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
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_alias')->label('Alias'),
|
||||
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) {
|
||||
false => 'tabler-star',
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
false => 'gray',
|
||||
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)
|
||||
@@ -66,94 +56,13 @@ class AllocationsRelationManager extends RelationManager
|
||||
->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'),
|
||||
Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||
//Tables\Actions\AssociateAction::make()->label('Add Allocation'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DissociateBulkAction::make(),
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\UserResource\Pages;
|
||||
use App\Filament\Resources\UserResource\RelationManagers\ServersRelationManager;
|
||||
use App\Filament\Resources\UserResource\RelationManagers;
|
||||
use App\Models\User;
|
||||
use Filament\Resources\Resource;
|
||||
|
||||
@@ -15,15 +15,10 @@ class UserResource extends Resource
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'username';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
ServersRelationManager::class,
|
||||
RelationManagers\ServersRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,6 +26,7 @@ class UserResource extends Resource
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
'create' => Pages\CreateUser::route('/create'),
|
||||
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
66
app/Filament/Resources/UserResource/Pages/CreateUser.php
Normal file
66
app/Filament/Resources/UserResource/Pages/CreateUser.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use App\Models\User;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Form;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class CreateUser extends CreateRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('username')->required()->maxLength(191),
|
||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(191),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||
->dehydrated(fn (?string $state): bool => filled($state))
|
||||
->required(fn (string $operation): bool => $operation === 'create')
|
||||
->password(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('root_admin')
|
||||
->label('Administrator (Root)')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Admin',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->disableOptionWhen(function (string $operation, $value, User $user) {
|
||||
if ($operation !== 'edit' || $value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->isLastRootAdmin();
|
||||
})
|
||||
->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '')
|
||||
->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '')
|
||||
->hintColor('warning')
|
||||
->inline()
|
||||
->required()
|
||||
->default(false),
|
||||
|
||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
||||
Forms\Components\Select::make('language')
|
||||
->required()
|
||||
->hidden()
|
||||
->default('en')
|
||||
->options(fn (User $user) => $user->getAvailableLanguages()),
|
||||
])->columns(2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,15 @@
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\ToggleTwoFactorService;
|
||||
use App\Services\Users\TwoFactorSetupService;
|
||||
use chillerlan\QRCode\Common\EccLevel;
|
||||
use chillerlan\QRCode\Common\Version;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use DateTimeZone;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
@@ -23,18 +20,13 @@ 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\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
/**
|
||||
* @method User getUser()
|
||||
*/
|
||||
class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
{
|
||||
protected function getForms(): array
|
||||
@@ -53,8 +45,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->label(trans('strings.username'))
|
||||
->disabled()
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->unique(ignoreRecord: true)
|
||||
->autofocus(),
|
||||
|
||||
@@ -63,7 +54,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->label(trans('strings.email'))
|
||||
->email()
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->maxLength(191)
|
||||
->unique(ignoreRecord: true),
|
||||
|
||||
TextInput::make('password')
|
||||
@@ -87,12 +78,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->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()
|
||||
@@ -114,32 +99,15 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
|
||||
if ($this->getUser()->use_totp) {
|
||||
return [
|
||||
Placeholder::make('2fa-already-enabled')
|
||||
->label('Two Factor Authentication is currently enabled!'),
|
||||
Textarea::make('backup-tokens')
|
||||
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->rows(10)
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->helperText('These will not be shown again!')
|
||||
->label('Backup Tokens:'),
|
||||
TextInput::make('2fa-disable-code')
|
||||
->label('Disable 2FA')
|
||||
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
|
||||
Placeholder::make('2FA already enabled!'),
|
||||
];
|
||||
}
|
||||
/** @var TwoFactorSetupService */
|
||||
$setupService = app(TwoFactorSetupService::class);
|
||||
|
||||
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
|
||||
"users.{$this->getUser()->id}.2fa.state",
|
||||
now()->addMinutes(5), fn () => $setupService->handle($this->getUser())
|
||||
);
|
||||
['image_url_data' => $url] = $setupService->handle($this->getUser());
|
||||
|
||||
$options = new QROptions([
|
||||
'svgLogo' => public_path('pelican.svg'),
|
||||
'svgLogoScale' => 0.05,
|
||||
'addLogoSpace' => true,
|
||||
'logoSpaceWidth' => 13,
|
||||
'logoSpaceHeight' => 13,
|
||||
@@ -147,24 +115,22 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
|
||||
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
|
||||
|
||||
// SVG logo options (see extended class)
|
||||
$options->svgLogo = public_path('pelican.svg'); // logo from: https://github.com/simple-icons/simple-icons
|
||||
$options->svgLogoScale = 0.05;
|
||||
// $options->svgLogoCssClass = 'dark';
|
||||
|
||||
// QROptions
|
||||
// @phpstan-ignore property.protected
|
||||
$options->version = Version::AUTO;
|
||||
// $options->outputInterface = QRSvgWithLogo::class;
|
||||
// @phpstan-ignore property.protected
|
||||
$options->outputBase64 = false;
|
||||
// @phpstan-ignore property.protected
|
||||
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
|
||||
// @phpstan-ignore property.protected
|
||||
$options->addQuietzone = true;
|
||||
// $options->drawLightModules = true;
|
||||
// @phpstan-ignore property.protected
|
||||
$options->connectPaths = true;
|
||||
// @phpstan-ignore property.protected
|
||||
$options->drawCircularModules = true;
|
||||
// $options->circleRadius = 0.45;
|
||||
|
||||
// @phpstan-ignore property.protected
|
||||
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
|
||||
<stop stop-color="#7dd4fc" offset="0"/>
|
||||
<stop stop-color="#38bdf8" offset="0.5"/>
|
||||
@@ -181,19 +147,9 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
Placeholder::make('qr')
|
||||
->label('Scan QR Code')
|
||||
->content(fn () => new HtmlString("
|
||||
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
|
||||
<div style='width: 300px'>$image</div>
|
||||
"))
|
||||
->helperText('Setup Key: '. $secret),
|
||||
TextInput::make('2facode')
|
||||
->label('Code')
|
||||
->requiredWith('2fapassword')
|
||||
->helperText('Scan the QR code above using your two-step authentication app, then enter the code generated.'),
|
||||
TextInput::make('2fapassword')
|
||||
->label('Current Password')
|
||||
->requiredWith('2facode')
|
||||
->currentPassword()
|
||||
->password()
|
||||
->helperText('Enter your current password to verify.'),
|
||||
->default('asdfasdf'),
|
||||
];
|
||||
}),
|
||||
|
||||
@@ -202,12 +158,8 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->schema([
|
||||
Grid::make('asdf')->columns(5)->schema([
|
||||
Section::make('Create API Key')->columnSpan(3)->schema([
|
||||
|
||||
TextInput::make('description')
|
||||
->live(),
|
||||
|
||||
TextInput::make('description'),
|
||||
TagsInput::make('allowed_ips')
|
||||
->live()
|
||||
->splitKeys([',', ' ', 'Tab'])
|
||||
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
||||
->label('Whitelisted IP\'s')
|
||||
@@ -215,10 +167,9 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->columnSpanFull(),
|
||||
])->headerActions([
|
||||
Action::make('Create')
|
||||
->disabled(fn (Get $get) => $get('description') === null)
|
||||
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
|
||||
->action(function (Get $get, Action $action, User $user) {
|
||||
$token = $user->createToken(
|
||||
->action(function (Get $get, Action $action) {
|
||||
$token = auth()->user()->createToken(
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
);
|
||||
@@ -231,9 +182,8 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
$action->success();
|
||||
}),
|
||||
]),
|
||||
Section::make('Keys')->columnSpan(2)->schema([
|
||||
Section::make('API Keys')->columnSpan(2)->schema([
|
||||
Repeater::make('keys')
|
||||
->label('')
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
@@ -285,43 +235,4 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloquent\Model
|
||||
{
|
||||
if ($token = $data['2facode'] ?? null) {
|
||||
/** @var ToggleTwoFactorService $service */
|
||||
$service = resolve(ToggleTwoFactorService::class);
|
||||
|
||||
$tokens = $service->handle($record, $token, true);
|
||||
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(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);
|
||||
|
||||
cache()->forget("users.$record->id.2fa.state");
|
||||
}
|
||||
|
||||
return parent::handleRecordUpdate($record, $data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
|
||||
Notification::make()
|
||||
->title('Invalid 2FA Code')
|
||||
->body($e->getMessage())
|
||||
->color('danger')
|
||||
->icon('tabler-2fa')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,12 @@
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use App\Models\User;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Form;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class EditUser extends EditRecord
|
||||
@@ -23,41 +19,54 @@ class EditUser extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
TextInput::make('username')->required()->maxLength(255),
|
||||
TextInput::make('email')->email()->required()->maxLength(255),
|
||||
TextInput::make('password')
|
||||
Forms\Components\TextInput::make('username')->required()->maxLength(191),
|
||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(191),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||
->dehydrated(fn (?string $state): bool => filled($state))
|
||||
->required(fn (string $operation): bool => $operation === 'create')
|
||||
->password(),
|
||||
Select::make('language')
|
||||
|
||||
Forms\Components\ToggleButtons::make('root_admin')
|
||||
->label('Administrator (Root)')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Admin',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->disableOptionWhen(function (string $operation, $value, User $user) {
|
||||
if ($operation !== 'edit' || $value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->isLastRootAdmin();
|
||||
})
|
||||
->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '')
|
||||
->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '')
|
||||
->hintColor('warning')
|
||||
->inline()
|
||||
->required()
|
||||
->default(false),
|
||||
|
||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
||||
|
||||
Forms\Components\Select::make('language')
|
||||
->required()
|
||||
->hidden()
|
||||
->default('en')
|
||||
->options(fn (User $user) => $user->getAvailableLanguages()),
|
||||
Hidden::make('skipValidation')->default(true),
|
||||
CheckboxList::make('roles')
|
||||
->disabled(fn (User $user) => $user->id === auth()->user()->id)
|
||||
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||
->relationship('roles', 'name')
|
||||
->label('Admin Roles')
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
|
||||
])->columns(),
|
||||
]);
|
||||
}
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete'))
|
||||
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,11 @@
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
@@ -29,105 +18,63 @@ class ListUsers extends ListRecords
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
ImageColumn::make('picture')
|
||||
Tables\Columns\ImageColumn::make('picture')
|
||||
->visibleFrom('lg')
|
||||
->label('')
|
||||
->extraImgAttributes(['class' => 'rounded-full'])
|
||||
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||
TextColumn::make('external_id')
|
||||
Tables\Columns\TextColumn::make('external_id')
|
||||
->searchable()
|
||||
->hidden(),
|
||||
TextColumn::make('uuid')
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
->label('UUID')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
TextColumn::make('username')
|
||||
Tables\Columns\TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
Tables\Columns\TextColumn::make('email')
|
||||
->searchable()
|
||||
->icon('tabler-mail'),
|
||||
IconColumn::make('use_totp')
|
||||
->label('2FA')
|
||||
Tables\Columns\IconColumn::make('root_admin')
|
||||
->visibleFrom('md')
|
||||
->label('Admin')
|
||||
->boolean()
|
||||
->trueIcon('tabler-star-filled')
|
||||
->falseIcon('tabler-star-off')
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('use_totp')->label('2FA')
|
||||
->visibleFrom('lg')
|
||||
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
||||
->boolean()->sortable(),
|
||||
TextColumn::make('roles_count')
|
||||
->counts('roles')
|
||||
->icon('tabler-users-group')
|
||||
->label('Roles')
|
||||
->formatStateUsing(fn (User $user, $state) => $state . ($user->isRootAdmin() ? ' (Root Admin)' : '')),
|
||||
TextColumn::make('servers_count')
|
||||
Tables\Columns\TextColumn::make('servers_count')
|
||||
->counts('servers')
|
||||
->icon('tabler-server')
|
||||
->label('Servers'),
|
||||
TextColumn::make('subusers_count')
|
||||
Tables\Columns\TextColumn::make('subusers_count')
|
||||
->visibleFrom('sm')
|
||||
->label('Subusers')
|
||||
->counts('subusers')
|
||||
->icon('tabler-users'),
|
||||
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (User $user) => !$user->servers_count)
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete user')),
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make('create')
|
||||
->label('Create User')
|
||||
->createAnother(false)
|
||||
->form([
|
||||
Grid::make()
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
->alphaNum()
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
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(),
|
||||
CheckboxList::make('roles')
|
||||
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||
->relationship('roles', 'name')
|
||||
->dehydrated()
|
||||
->label('Admin Roles')
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
]),
|
||||
])
|
||||
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
||||
->action(function (array $data) {
|
||||
$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);
|
||||
|
||||
Notification::make()
|
||||
->title('User Created!')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect()->route('filament.admin.resources.users.index');
|
||||
}),
|
||||
Actions\CreateAction::make()
|
||||
->label('Create User'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class ServersRelationManager extends RelationManager
|
||||
->sortable(),
|
||||
Tables\Columns\SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('image')->hidden(),
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin\Eggs;
|
||||
|
||||
use App\Exceptions\Service\Egg\NoParentConfigurationFoundException;
|
||||
use Illuminate\View\View;
|
||||
use App\Models\Egg;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Illuminate\View\Factory as ViewFactory;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Eggs\EggUpdateService;
|
||||
use App\Services\Eggs\EggCreationService;
|
||||
use App\Http\Requests\Admin\Egg\EggFormRequest;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class EggController extends Controller
|
||||
{
|
||||
@@ -19,6 +19,8 @@ class EggController extends Controller
|
||||
*/
|
||||
public function __construct(
|
||||
protected AlertsMessageBag $alert,
|
||||
protected EggCreationService $creationService,
|
||||
protected EggUpdateService $updateService,
|
||||
protected ViewFactory $view
|
||||
) {
|
||||
}
|
||||
@@ -56,16 +58,7 @@ class EggController extends Controller
|
||||
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
|
||||
$data['author'] = $request->user()->email;
|
||||
|
||||
$data['config_from'] = array_get($data, 'config_from');
|
||||
if (!is_null($data['config_from'])) {
|
||||
$parentEgg = Egg::query()->find(array_get($data, 'config_from'));
|
||||
throw_unless($parentEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
|
||||
}
|
||||
|
||||
$egg = Egg::query()->create(array_merge($data, [
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
]));
|
||||
|
||||
$egg = $this->creationService->handle($data);
|
||||
$this->alert->success(trans('admin/eggs.notices.egg_created'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', $egg->id);
|
||||
@@ -97,13 +90,7 @@ class EggController extends Controller
|
||||
$data = $request->validated();
|
||||
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
|
||||
|
||||
$eggId = array_get($data, 'config_from');
|
||||
$copiedFromEgg = Egg::query()->find($eggId);
|
||||
|
||||
throw_unless($copiedFromEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
|
||||
|
||||
$egg->update($data);
|
||||
|
||||
$this->updateService->handle($egg, $data);
|
||||
$this->alert->success(trans('admin/eggs.notices.updated'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', $egg->id);
|
||||
|
||||
@@ -10,6 +10,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use App\Http\Requests\Admin\Egg\EggImportFormRequest;
|
||||
use App\Services\Eggs\Sharing\EggUpdateImporterService;
|
||||
|
||||
class EggShareController extends Controller
|
||||
{
|
||||
@@ -20,6 +21,7 @@ class EggShareController extends Controller
|
||||
protected AlertsMessageBag $alert,
|
||||
protected EggExporterService $exporterService,
|
||||
protected EggImporterService $importerService,
|
||||
protected EggUpdateImporterService $updateImporterService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -44,7 +46,7 @@ class EggShareController extends Controller
|
||||
*/
|
||||
public function import(EggImportFormRequest $request): RedirectResponse
|
||||
{
|
||||
$egg = $this->importerService->fromFile($request->file('import_file'));
|
||||
$egg = $this->importerService->handle($request->file('import_file'));
|
||||
$this->alert->success(trans('admin/eggs.notices.imported'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', ['egg' => $egg->id]);
|
||||
@@ -59,7 +61,7 @@ class EggShareController extends Controller
|
||||
*/
|
||||
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
|
||||
{
|
||||
$this->importerService->fromFile($request->file('import_file'), $egg);
|
||||
$this->updateImporterService->handle($egg, $request->file('import_file'));
|
||||
$this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash();
|
||||
|
||||
return redirect()->route('admin.eggs.view', ['egg' => $egg]);
|
||||
|
||||
@@ -56,7 +56,7 @@ class NodeAutoDeployController extends Controller
|
||||
|
||||
return new JsonResponse([
|
||||
'node' => $node->id,
|
||||
'token' => $key->identifier . $key->token,
|
||||
'token' => $key->identifier . decrypt($key->token),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Admin\Nodes;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -12,7 +13,7 @@ class NodeController extends Controller
|
||||
/**
|
||||
* Returns a listing of nodes on the system.
|
||||
*/
|
||||
public function index(): View
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$nodes = QueryBuilder::for(
|
||||
Node::query()->withCount('servers')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user