mirror of
				https://github.com/pelican-dev/panel.git
				synced 2025-11-04 11:26:52 +01:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/main' into charles/nuke-node-stats
This commit is contained in:
		
						commit
						8c52209d5f
					
				@ -3,5 +3,4 @@ APP_DEBUG=false
 | 
			
		||||
APP_KEY=
 | 
			
		||||
APP_URL=http://panel.test
 | 
			
		||||
APP_INSTALLED=false
 | 
			
		||||
APP_TIMEZONE=UTC
 | 
			
		||||
APP_LOCALE=en
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/CODEOWNERS
									
									
									
									
										vendored
									
									
								
							@ -1,15 +0,0 @@
 | 
			
		||||
# Lines starting with '#' are comments.
 | 
			
		||||
# Each line is a file pattern followed by one or more owners.
 | 
			
		||||
 | 
			
		||||
# More details are here: https://help.github.com/articles/about-codeowners/
 | 
			
		||||
 | 
			
		||||
# The '*' pattern is global owners.
 | 
			
		||||
 | 
			
		||||
# Order is important. The last matching pattern has the most precedence.
 | 
			
		||||
# The folders are ordered as follows:
 | 
			
		||||
 | 
			
		||||
# In each subsection folders are ordered first by depth, then alphabetically.
 | 
			
		||||
# This should make it easy to add new rules without breaking existing ones.
 | 
			
		||||
 | 
			
		||||
# Global
 | 
			
		||||
* @pelican-dev/panel
 | 
			
		||||
							
								
								
									
										76
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										76
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							@ -213,3 +213,79 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Integration tests
 | 
			
		||||
        run: vendor/bin/pest tests/Integration
 | 
			
		||||
 | 
			
		||||
  postgresql:
 | 
			
		||||
    name: PostgreSQL
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        php: [8.2, 8.3, 8.4]
 | 
			
		||||
        database: ["postgres:14"]
 | 
			
		||||
    services:
 | 
			
		||||
      database:
 | 
			
		||||
        image: ${{ matrix.database }}
 | 
			
		||||
        env:
 | 
			
		||||
          POSTGRES_DB: testing
 | 
			
		||||
          POSTGRES_USER: postgres
 | 
			
		||||
          POSTGRES_PASSWORD: postgres
 | 
			
		||||
          POSTGRES_HOST_AUTH_METHOD: trust
 | 
			
		||||
        ports:
 | 
			
		||||
          - 5432:5432
 | 
			
		||||
        options: >-
 | 
			
		||||
          --health-cmd pg_isready
 | 
			
		||||
          --health-interval 10s
 | 
			
		||||
          --health-timeout 5s
 | 
			
		||||
          --health-retries 5
 | 
			
		||||
    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: pgsql
 | 
			
		||||
      DB_HOST: 127.0.0.1
 | 
			
		||||
      DB_DATABASE: testing
 | 
			
		||||
      DB_USERNAME: postgres
 | 
			
		||||
      DB_PASSWORD: postgres
 | 
			
		||||
      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: |
 | 
			
		||||
 | 
			
		||||
      - name: Setup PHP
 | 
			
		||||
        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 --no-progress --no-scripts
 | 
			
		||||
 | 
			
		||||
      - name: Unit tests
 | 
			
		||||
        run: vendor/bin/pest tests/Unit
 | 
			
		||||
        env:
 | 
			
		||||
          DB_HOST: UNIT_NO_DB
 | 
			
		||||
          SKIP_MIGRATIONS: true
 | 
			
		||||
 | 
			
		||||
      - name: Integration tests
 | 
			
		||||
        run: vendor/bin/pest tests/Integration
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/lint.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/lint.yaml
									
									
									
									
										vendored
									
									
								
							@ -35,7 +35,7 @@ jobs:
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        php: [8.2, 8.3, 8.4]
 | 
			
		||||
        php: [ 8.2, 8.3, 8.4 ]
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Code Checkout
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
@ -68,4 +68,4 @@ jobs:
 | 
			
		||||
        run: composer install --no-interaction --no-suggest --no-progress --no-scripts
 | 
			
		||||
 | 
			
		||||
      - name: PHPStan
 | 
			
		||||
        run: vendor/bin/phpstan --memory-limit=-1
 | 
			
		||||
        run: vendor/bin/phpstan --memory-limit=-1 --error-format=github
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,7 +1,6 @@
 | 
			
		||||
/.phpunit.cache
 | 
			
		||||
/node_modules
 | 
			
		||||
/public/build
 | 
			
		||||
/public/hot
 | 
			
		||||
/public/storage
 | 
			
		||||
/storage/*.key
 | 
			
		||||
/storage/pail
 | 
			
		||||
@ -24,8 +23,7 @@ yarn-error.log
 | 
			
		||||
/.vscode
 | 
			
		||||
 | 
			
		||||
public/assets/manifest.json
 | 
			
		||||
/database/*.sqlite
 | 
			
		||||
/database/*.sqlite-journal
 | 
			
		||||
/database/*.sqlite*
 | 
			
		||||
filament-monaco-editor/
 | 
			
		||||
_ide_helper*
 | 
			
		||||
/.phpstorm.meta.php
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								Dockerfile
									
									
									
									
									
								
							@ -1,16 +1,9 @@
 | 
			
		||||
# syntax=docker.io/docker/dockerfile:1.13-labs
 | 
			
		||||
# Pelican Production Dockerfile
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# For those who want to build this Dockerfile themselves, uncomment lines 6-12 and replace "localhost:5000/base-php:$TARGETARCH" on lines 17 and 67 with "base".
 | 
			
		||||
 | 
			
		||||
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine as base
 | 
			
		||||
 | 
			
		||||
# ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
 | 
			
		||||
 | 
			
		||||
# RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql
 | 
			
		||||
 | 
			
		||||
# RUN rm /usr/local/bin/install-php-extensions
 | 
			
		||||
##
 | 
			
		||||
#  If you want to build this locally you want to run `docker build -f Dockerfile.dev`
 | 
			
		||||
##
 | 
			
		||||
 | 
			
		||||
# ================================
 | 
			
		||||
# Stage 1-1: Composer Install
 | 
			
		||||
@ -82,13 +75,16 @@ RUN chown root:www-data ./ \
 | 
			
		||||
    && chmod 750 ./ \
 | 
			
		||||
    # Files should not have execute set, but directories need it
 | 
			
		||||
    && find ./ -type d -exec chmod 750 {} \; \
 | 
			
		||||
    # Symlink to env/database path, as www-data won't be able to write to webroot
 | 
			
		||||
    # Create necessary directories
 | 
			
		||||
    && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
 | 
			
		||||
    # Symlinks for env, database, and avatars
 | 
			
		||||
    && ln -s /pelican-data/.env ./.env \
 | 
			
		||||
    && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
 | 
			
		||||
    # Create necessary directories
 | 
			
		||||
    && mkdir -p /pelican-data /var/run/supervisord /etc/supercronic \
 | 
			
		||||
    # Finally allow www-data write permissions where necessary
 | 
			
		||||
    && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
 | 
			
		||||
    && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
 | 
			
		||||
    && ln -s  /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
 | 
			
		||||
    && ln -s  /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
 | 
			
		||||
    # Allow www-data write permissions where necessary
 | 
			
		||||
    && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
 | 
			
		||||
    && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
 | 
			
		||||
 | 
			
		||||
# Configure Supervisor
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
# ================================
 | 
			
		||||
# Stage 0: Build PHP Base Image
 | 
			
		||||
# ================================
 | 
			
		||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
 | 
			
		||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
 | 
			
		||||
 | 
			
		||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
 | 
			
		||||
 | 
			
		||||
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql
 | 
			
		||||
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
 | 
			
		||||
 | 
			
		||||
RUN rm /usr/local/bin/install-php-extensions
 | 
			
		||||
RUN rm /usr/local/bin/install-php-extensions
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										112
									
								
								Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Dockerfile.dev
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
			
		||||
# syntax=docker.io/docker/dockerfile:1.13-labs
 | 
			
		||||
# Pelican Development Dockerfile
 | 
			
		||||
 | 
			
		||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
 | 
			
		||||
 | 
			
		||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
 | 
			
		||||
 | 
			
		||||
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
 | 
			
		||||
 | 
			
		||||
RUN rm /usr/local/bin/install-php-extensions
 | 
			
		||||
 | 
			
		||||
# ================================
 | 
			
		||||
# Stage 1-1: Composer Install
 | 
			
		||||
# ================================
 | 
			
		||||
FROM --platform=$TARGETOS/$TARGETARCH base AS composer
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
 | 
			
		||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
 | 
			
		||||
 | 
			
		||||
# Copy bare minimum to install Composer dependencies
 | 
			
		||||
COPY composer.json composer.lock ./
 | 
			
		||||
 | 
			
		||||
RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
 | 
			
		||||
 | 
			
		||||
# ================================
 | 
			
		||||
# Stage 1-2: Yarn Install
 | 
			
		||||
# ================================
 | 
			
		||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
 | 
			
		||||
# Copy bare minimum to install Yarn dependencies
 | 
			
		||||
COPY package.json yarn.lock ./
 | 
			
		||||
 | 
			
		||||
RUN yarn config set network-timeout 300000 \
 | 
			
		||||
    && yarn install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
# ================================
 | 
			
		||||
# Stage 2-1: Composer Optimize
 | 
			
		||||
# ================================
 | 
			
		||||
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
 | 
			
		||||
 | 
			
		||||
# Copy full code to optimize autoload
 | 
			
		||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
 | 
			
		||||
 | 
			
		||||
RUN composer dump-autoload --optimize
 | 
			
		||||
 | 
			
		||||
# ================================
 | 
			
		||||
# Stage 2-2: Build Frontend Assets
 | 
			
		||||
# ================================
 | 
			
		||||
FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
 | 
			
		||||
# Copy full code
 | 
			
		||||
COPY --exclude=Caddyfile --exclude=docker/ . ./
 | 
			
		||||
COPY --from=composer /build .
 | 
			
		||||
 | 
			
		||||
RUN yarn run build
 | 
			
		||||
 | 
			
		||||
# ================================
 | 
			
		||||
# Stage 5: Build Final Application Image
 | 
			
		||||
# ================================
 | 
			
		||||
FROM --platform=$TARGETOS/$TARGETARCH base AS final
 | 
			
		||||
 | 
			
		||||
WORKDIR /var/www/html
 | 
			
		||||
 | 
			
		||||
# Install additional required libraries
 | 
			
		||||
RUN apk update && apk add --no-cache \
 | 
			
		||||
    caddy ca-certificates supervisor supercronic
 | 
			
		||||
 | 
			
		||||
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
 | 
			
		||||
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
 | 
			
		||||
 | 
			
		||||
# Set permissions
 | 
			
		||||
# First ensure all files are owned by root and restrict www-data to read access
 | 
			
		||||
RUN chown root:www-data ./ \
 | 
			
		||||
    && chmod 750 ./ \
 | 
			
		||||
    # Files should not have execute set, but directories need it
 | 
			
		||||
    && find ./ -type d -exec chmod 750 {} \; \
 | 
			
		||||
    # Create necessary directories
 | 
			
		||||
    && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
 | 
			
		||||
    # Symlinks for env, database, and avatars
 | 
			
		||||
    && ln -s /pelican-data/.env ./.env \
 | 
			
		||||
    && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
 | 
			
		||||
    && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
 | 
			
		||||
    && ln -s  /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
 | 
			
		||||
    && ln -s  /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
 | 
			
		||||
    # Allow www-data write permissions where necessary
 | 
			
		||||
    && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
 | 
			
		||||
    && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
 | 
			
		||||
 | 
			
		||||
# Configure Supervisor
 | 
			
		||||
COPY docker/supervisord.conf /etc/supervisord.conf
 | 
			
		||||
COPY docker/Caddyfile /etc/caddy/Caddyfile
 | 
			
		||||
# Add Laravel scheduler to crontab
 | 
			
		||||
COPY docker/crontab /etc/supercronic/crontab
 | 
			
		||||
 | 
			
		||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
 | 
			
		||||
 | 
			
		||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
 | 
			
		||||
  CMD curl -f http://localhost/up || exit 1
 | 
			
		||||
 | 
			
		||||
EXPOSE 80 443
 | 
			
		||||
 | 
			
		||||
VOLUME /pelican-data
 | 
			
		||||
 | 
			
		||||
USER www-data
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
 | 
			
		||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
 | 
			
		||||
@ -14,9 +14,9 @@ class NodeVersionsCheck extends Check
 | 
			
		||||
 | 
			
		||||
    public function run(): Result
 | 
			
		||||
    {
 | 
			
		||||
        $all = Node::query()->count();
 | 
			
		||||
        $all = Node::all();
 | 
			
		||||
 | 
			
		||||
        if ($all === 0) {
 | 
			
		||||
        if ($all->isEmpty()) {
 | 
			
		||||
            $result = Result::make()
 | 
			
		||||
                ->notificationMessage(trans('admin/health.results.nodeversions.no_nodes_created'))
 | 
			
		||||
                ->shortSummary(trans('admin/health.results.nodeversions.no_nodes'));
 | 
			
		||||
@ -25,16 +25,18 @@ class NodeVersionsCheck extends Check
 | 
			
		||||
            return $result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $latestVersion = $this->versionService->latestWingsVersion();
 | 
			
		||||
 | 
			
		||||
        $outdated = Node::query()->get()
 | 
			
		||||
            ->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && $node->systemInformation()['version'] !== $latestVersion)
 | 
			
		||||
        $outdated = $all
 | 
			
		||||
            ->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && !$this->versionService->isLatestWings($node->systemInformation()['version']))
 | 
			
		||||
            ->count();
 | 
			
		||||
 | 
			
		||||
        $all = $all->count();
 | 
			
		||||
        $latestVersion = $this->versionService->latestWingsVersion();
 | 
			
		||||
 | 
			
		||||
        $result = Result::make()
 | 
			
		||||
            ->meta([
 | 
			
		||||
                'all' => $all,
 | 
			
		||||
                'outdated' => $outdated,
 | 
			
		||||
                'latestVersion' => $latestVersion,
 | 
			
		||||
            ])
 | 
			
		||||
            ->shortSummary($outdated === 0 ? trans('admin/health.results.nodeversions.all_up_to_date') : trans('admin/health.results.nodeversions.outdated', ['outdated' => $outdated, 'all' => $all]));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@
 | 
			
		||||
namespace App\Console\Commands\Environment;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Console\Command;
 | 
			
		||||
use Illuminate\Support\Facades\Artisan;
 | 
			
		||||
 | 
			
		||||
class AppSettingsCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
@ -21,9 +20,13 @@ class AppSettingsCommand extends Command
 | 
			
		||||
 | 
			
		||||
        if (!config('app.key')) {
 | 
			
		||||
            $this->comment('Generating app key');
 | 
			
		||||
            Artisan::call('key:generate');
 | 
			
		||||
            $this->call('key:generate');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Artisan::call('filament:optimize');
 | 
			
		||||
        $this->comment('Creating storage link');
 | 
			
		||||
        $this->call('storage:link');
 | 
			
		||||
 | 
			
		||||
        $this->comment('Caching components & icons');
 | 
			
		||||
        $this->call('filament:optimize');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ class RedisSetupCommand extends Command
 | 
			
		||||
    {
 | 
			
		||||
        $this->variables['CACHE_STORE'] = 'redis';
 | 
			
		||||
        $this->variables['QUEUE_CONNECTION'] = 'redis';
 | 
			
		||||
        $this->variables['SESSION_DRIVERS'] = 'redis';
 | 
			
		||||
        $this->variables['SESSION_DRIVER'] = 'redis';
 | 
			
		||||
 | 
			
		||||
        $this->requestRedisSettings();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Console\Command;
 | 
			
		||||
use Illuminate\Contracts\Filesystem\Filesystem;
 | 
			
		||||
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
 | 
			
		||||
use SplFileInfo;
 | 
			
		||||
 | 
			
		||||
class CleanServiceBackupFilesCommand extends Command
 | 
			
		||||
{
 | 
			
		||||
@ -32,9 +33,10 @@ class CleanServiceBackupFilesCommand extends Command
 | 
			
		||||
     */
 | 
			
		||||
    public function handle(): void
 | 
			
		||||
    {
 | 
			
		||||
        /** @var SplFileInfo[] */
 | 
			
		||||
        $files = $this->disk->files('services/.bak');
 | 
			
		||||
 | 
			
		||||
        collect($files)->each(function (\SplFileInfo $file) {
 | 
			
		||||
        collect($files)->each(function ($file) {
 | 
			
		||||
            $lastModified = Carbon::createFromTimestamp($this->disk->lastModified($file->getPath()));
 | 
			
		||||
            if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) {
 | 
			
		||||
                $this->disk->delete($file->getPath());
 | 
			
		||||
 | 
			
		||||
@ -30,8 +30,11 @@ class Kernel extends ConsoleKernel
 | 
			
		||||
     */
 | 
			
		||||
    protected function schedule(Schedule $schedule): void
 | 
			
		||||
    {
 | 
			
		||||
        // https://laravel.com/docs/10.x/upgrade#redis-cache-tags
 | 
			
		||||
        $schedule->command('cache:prune-stale-tags')->hourly();
 | 
			
		||||
        if (config('cache.default') === 'redis') {
 | 
			
		||||
            // https://laravel.com/docs/10.x/upgrade#redis-cache-tags
 | 
			
		||||
            // This only needs to run when using redis. anything else throws an error.
 | 
			
		||||
            $schedule->command('cache:prune-stale-tags')->hourly();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Execute scheduled commands for servers every minute, as if there was a normal cron running.
 | 
			
		||||
        $schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								app/Enums/BackupStatus.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/Enums/BackupStatus.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Enums;
 | 
			
		||||
 | 
			
		||||
use Filament\Support\Contracts\HasColor;
 | 
			
		||||
use Filament\Support\Contracts\HasIcon;
 | 
			
		||||
use Filament\Support\Contracts\HasLabel;
 | 
			
		||||
 | 
			
		||||
enum BackupStatus: string implements HasColor, HasIcon, HasLabel
 | 
			
		||||
{
 | 
			
		||||
    case InProgress = 'in_progress';
 | 
			
		||||
    case Successful = 'successful';
 | 
			
		||||
    case Failed = 'failed';
 | 
			
		||||
 | 
			
		||||
    public function getIcon(): string
 | 
			
		||||
    {
 | 
			
		||||
        return match ($this) {
 | 
			
		||||
            self::InProgress => 'tabler-circle-dashed',
 | 
			
		||||
            self::Successful => 'tabler-circle-check',
 | 
			
		||||
            self::Failed => 'tabler-circle-x',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getColor(): string
 | 
			
		||||
    {
 | 
			
		||||
        return match ($this) {
 | 
			
		||||
            self::InProgress => 'primary',
 | 
			
		||||
            self::Successful => 'success',
 | 
			
		||||
            self::Failed => 'danger',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): string
 | 
			
		||||
    {
 | 
			
		||||
        return str($this->value)->headline();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								app/Enums/ConsoleWidgetPosition.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/Enums/ConsoleWidgetPosition.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Enums;
 | 
			
		||||
 | 
			
		||||
enum ConsoleWidgetPosition: string
 | 
			
		||||
{
 | 
			
		||||
    case Top = 'top';
 | 
			
		||||
    case AboveConsole = 'above_console';
 | 
			
		||||
    case BelowConsole = 'below_console';
 | 
			
		||||
    case Bottom = 'bottom';
 | 
			
		||||
}
 | 
			
		||||
@ -62,7 +62,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
 | 
			
		||||
            self::Removing => 'warning',
 | 
			
		||||
            self::Missing => 'danger',
 | 
			
		||||
            self::Stopping => 'warning',
 | 
			
		||||
            self::Offline => 'gray',
 | 
			
		||||
            self::Offline => 'danger',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -14,4 +14,24 @@ enum RolePermissionModels: string
 | 
			
		||||
    case Server = 'server';
 | 
			
		||||
    case User = 'user';
 | 
			
		||||
    case Webhook = 'webhook';
 | 
			
		||||
 | 
			
		||||
    public function viewAny(): string
 | 
			
		||||
    {
 | 
			
		||||
        return RolePermissionPrefixes::ViewAny->value . ' ' . $this->value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function view(): string
 | 
			
		||||
    {
 | 
			
		||||
        return RolePermissionPrefixes::View->value . ' ' . $this->value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function create(): string
 | 
			
		||||
    {
 | 
			
		||||
        return RolePermissionPrefixes::Create->value . ' ' . $this->value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function update(): string
 | 
			
		||||
    {
 | 
			
		||||
        return RolePermissionPrefixes::Update->value . ' ' . $this->value;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								app/Exceptions/Repository/FileNotEditableException.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/Exceptions/Repository/FileNotEditableException.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Exceptions\Repository;
 | 
			
		||||
 | 
			
		||||
use Exception;
 | 
			
		||||
 | 
			
		||||
class FileNotEditableException extends Exception {}
 | 
			
		||||
							
								
								
									
										42
									
								
								app/Extensions/Avatar/AvatarProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/Extensions/Avatar/AvatarProvider.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Avatar;
 | 
			
		||||
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use Illuminate\Support\Arr;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
 | 
			
		||||
abstract class AvatarProvider
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array<string, static>
 | 
			
		||||
     */
 | 
			
		||||
    protected static array $providers = [];
 | 
			
		||||
 | 
			
		||||
    public static function getProvider(string $id): ?self
 | 
			
		||||
    {
 | 
			
		||||
        return Arr::get(static::$providers, $id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return array<string, static>
 | 
			
		||||
     */
 | 
			
		||||
    public static function getAll(): array
 | 
			
		||||
    {
 | 
			
		||||
        return static::$providers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        static::$providers[$this->getId()] = $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract public function getId(): string;
 | 
			
		||||
 | 
			
		||||
    abstract public function get(User $user): ?string;
 | 
			
		||||
 | 
			
		||||
    public function getName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return Str::title($this->getId());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								app/Extensions/Avatar/Providers/GravatarProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/Extensions/Avatar/Providers/GravatarProvider.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Avatar\Providers;
 | 
			
		||||
 | 
			
		||||
use App\Extensions\Avatar\AvatarProvider;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
 | 
			
		||||
class GravatarProvider extends AvatarProvider
 | 
			
		||||
{
 | 
			
		||||
    public function getId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'gravatar';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function get(User $user): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'https://gravatar.com/avatar/' . md5($user->email);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function register(): self
 | 
			
		||||
    {
 | 
			
		||||
        return new self();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								app/Extensions/Avatar/Providers/UiAvatarsProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/Extensions/Avatar/Providers/UiAvatarsProvider.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Avatar\Providers;
 | 
			
		||||
 | 
			
		||||
use App\Extensions\Avatar\AvatarProvider;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
 | 
			
		||||
class UiAvatarsProvider extends AvatarProvider
 | 
			
		||||
{
 | 
			
		||||
    public function getId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'uiavatars';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'UI Avatars';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function get(User $user): ?string
 | 
			
		||||
    {
 | 
			
		||||
        // UI Avatars is the default of filament so just return null here
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function register(): self
 | 
			
		||||
    {
 | 
			
		||||
        return new self();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions;
 | 
			
		||||
 | 
			
		||||
use App\Models\DatabaseHost;
 | 
			
		||||
 | 
			
		||||
class DynamicDatabaseConnection
 | 
			
		||||
{
 | 
			
		||||
    public const DB_CHARSET = 'utf8';
 | 
			
		||||
 | 
			
		||||
    public const DB_COLLATION = 'utf8_unicode_ci';
 | 
			
		||||
 | 
			
		||||
    public const DB_DRIVER = 'mysql';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adds a dynamic database connection entry to the runtime config.
 | 
			
		||||
     */
 | 
			
		||||
    public function set(string $connection, DatabaseHost|int $host, string $database = 'mysql'): void
 | 
			
		||||
    {
 | 
			
		||||
        if (!$host instanceof DatabaseHost) {
 | 
			
		||||
            $host = DatabaseHost::query()->findOrFail($host);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        config()->set('database.connections.' . $connection, [
 | 
			
		||||
            'driver' => self::DB_DRIVER,
 | 
			
		||||
            'host' => $host->host,
 | 
			
		||||
            'port' => $host->port,
 | 
			
		||||
            'database' => $database,
 | 
			
		||||
            'username' => $host->username,
 | 
			
		||||
            'password' => $host->password,
 | 
			
		||||
            'charset' => self::DB_CHARSET,
 | 
			
		||||
            'collation' => self::DB_COLLATION,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								app/Extensions/Features/FeatureProvider.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/Extensions/Features/FeatureProvider.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Features;
 | 
			
		||||
 | 
			
		||||
use Filament\Actions\Action;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
 | 
			
		||||
abstract class FeatureProvider
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * @var array<string, static>
 | 
			
		||||
     */
 | 
			
		||||
    protected static array $providers = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param  string[]  $id
 | 
			
		||||
     * @return self|static[]
 | 
			
		||||
     */
 | 
			
		||||
    public static function getProviders(string|array|null $id = null): array|self
 | 
			
		||||
    {
 | 
			
		||||
        if (is_array($id)) {
 | 
			
		||||
            return array_intersect_key(static::$providers, array_flip($id));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $id ? static::$providers[$id] : static::$providers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function __construct(protected Application $app)
 | 
			
		||||
    {
 | 
			
		||||
        if (array_key_exists($this->getId(), static::$providers)) {
 | 
			
		||||
            if (!$this->app->runningUnitTests()) {
 | 
			
		||||
                logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        static::$providers[$this->getId()] = $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abstract public function getId(): string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A matching subset string (case-insensitive) from the console output
 | 
			
		||||
     *
 | 
			
		||||
     * @return array<string>
 | 
			
		||||
     */
 | 
			
		||||
    abstract public function getListeners(): array;
 | 
			
		||||
 | 
			
		||||
    abstract public function getAction(): Action;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										127
									
								
								app/Extensions/Features/GSLToken.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								app/Extensions/Features/GSLToken.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,127 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Features;
 | 
			
		||||
 | 
			
		||||
use App\Facades\Activity;
 | 
			
		||||
use App\Models\Permission;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Models\ServerVariable;
 | 
			
		||||
use App\Repositories\Daemon\DaemonPowerRepository;
 | 
			
		||||
use Closure;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Filament\Actions\Action;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
use Illuminate\Support\Facades\Blade;
 | 
			
		||||
use Illuminate\Support\Facades\Validator;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
 | 
			
		||||
class GSLToken extends FeatureProvider
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(protected Application $app)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return array<string> */
 | 
			
		||||
    public function getListeners(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            '(gsl token expired)',
 | 
			
		||||
            '(account not found)',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'gsl_token';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAction(): Action
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        /** @var ServerVariable $serverVariable */
 | 
			
		||||
        $serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
 | 
			
		||||
            $query->where('env_variable', 'STEAM_ACC');
 | 
			
		||||
        })->first();
 | 
			
		||||
 | 
			
		||||
        return Action::make($this->getId())
 | 
			
		||||
            ->requiresConfirmation()
 | 
			
		||||
            ->modalHeading('Invalid GSL token')
 | 
			
		||||
            ->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
 | 
			
		||||
            ->modalSubmitActionLabel('Update GSL Token')
 | 
			
		||||
            ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
 | 
			
		||||
            ->form([
 | 
			
		||||
                Placeholder::make('info')
 | 
			
		||||
                    ->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
 | 
			
		||||
                TextInput::make('gsltoken')
 | 
			
		||||
                    ->label('GSL Token')
 | 
			
		||||
                    ->rules([
 | 
			
		||||
                        fn (): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
 | 
			
		||||
                            $validator = Validator::make(['validatorkey' => $value], [
 | 
			
		||||
                                'validatorkey' => $serverVariable->variable->rules,
 | 
			
		||||
                            ]);
 | 
			
		||||
 | 
			
		||||
                            if ($validator->fails()) {
 | 
			
		||||
                                $message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
 | 
			
		||||
 | 
			
		||||
                                $fail($message);
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->hintIcon('tabler-code')
 | 
			
		||||
                    ->label(fn () => $serverVariable->variable->name)
 | 
			
		||||
                    ->hintIconTooltip(fn () => implode('|', $serverVariable->variable->rules))
 | 
			
		||||
                    ->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
 | 
			
		||||
                    ->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
 | 
			
		||||
            ])
 | 
			
		||||
            ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
 | 
			
		||||
                /** @var Server $server */
 | 
			
		||||
                $server = Filament::getTenant();
 | 
			
		||||
                try {
 | 
			
		||||
                    $new = $data['gsltoken'] ?? '';
 | 
			
		||||
                    $original = $serverVariable->variable_value;
 | 
			
		||||
 | 
			
		||||
                    $serverVariable->update([
 | 
			
		||||
                        'variable_value' => $new,
 | 
			
		||||
                    ]);
 | 
			
		||||
 | 
			
		||||
                    if ($original !== $new) {
 | 
			
		||||
 | 
			
		||||
                        Activity::event('server:startup.edit')
 | 
			
		||||
                            ->property([
 | 
			
		||||
                                'variable' => $serverVariable->variable->env_variable,
 | 
			
		||||
                                'old' => $original,
 | 
			
		||||
                                'new' => $new,
 | 
			
		||||
                            ])
 | 
			
		||||
                            ->log();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $powerRepository->setServer($server)->send('restart');
 | 
			
		||||
 | 
			
		||||
                    Notification::make()
 | 
			
		||||
                        ->title('GSL Token updated')
 | 
			
		||||
                        ->body('Server will restart now.')
 | 
			
		||||
                        ->success()
 | 
			
		||||
                        ->send();
 | 
			
		||||
                } catch (Exception $exception) {
 | 
			
		||||
                    Notification::make()
 | 
			
		||||
                        ->title('Could not update GSL Token')
 | 
			
		||||
                        ->body($exception->getMessage())
 | 
			
		||||
                        ->danger()
 | 
			
		||||
                        ->send();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function register(Application $app): self
 | 
			
		||||
    {
 | 
			
		||||
        return new self($app);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								app/Extensions/Features/JavaVersion.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								app/Extensions/Features/JavaVersion.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Features;
 | 
			
		||||
 | 
			
		||||
use App\Facades\Activity;
 | 
			
		||||
use App\Models\Permission;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Repositories\Daemon\DaemonPowerRepository;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Filament\Actions\Action;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\Select;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
 | 
			
		||||
class JavaVersion extends FeatureProvider
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(protected Application $app)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return array<string> */
 | 
			
		||||
    public function getListeners(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'java.lang.UnsupportedClassVersionError',
 | 
			
		||||
            'unsupported major.minor version',
 | 
			
		||||
            'has been compiled by a more recent version of the java runtime',
 | 
			
		||||
            'minecraft 1.17 requires running the server with java 16 or above',
 | 
			
		||||
            'minecraft 1.18 requires running the server with java 17 or above',
 | 
			
		||||
            'minecraft 1.19 requires running the server with java 17 or above',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'java_version';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAction(): Action
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        return Action::make($this->getId())
 | 
			
		||||
            ->requiresConfirmation()
 | 
			
		||||
            ->modalHeading('Unsupported Java Version')
 | 
			
		||||
            ->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
 | 
			
		||||
            ->modalSubmitActionLabel('Update Docker Image')
 | 
			
		||||
            ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
 | 
			
		||||
            ->form([
 | 
			
		||||
                Placeholder::make('java')
 | 
			
		||||
                    ->label('Please select a supported version from the list below to continue starting the server.'),
 | 
			
		||||
                Select::make('image')
 | 
			
		||||
                    ->label('Docker Image')
 | 
			
		||||
                    ->disabled(fn () => !in_array($server->image, $server->egg->docker_images))
 | 
			
		||||
                    ->options(fn () => collect($server->egg->docker_images)->mapWithKeys(fn ($key, $value) => [$key => $value]))
 | 
			
		||||
                    ->selectablePlaceholder(false)
 | 
			
		||||
                    ->default(fn () => $server->image)
 | 
			
		||||
                    ->notIn(fn () => $server->image)
 | 
			
		||||
                    ->required()
 | 
			
		||||
                    ->preload()
 | 
			
		||||
                    ->native(false),
 | 
			
		||||
            ])
 | 
			
		||||
            ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
 | 
			
		||||
                try {
 | 
			
		||||
                    $new = $data['image'];
 | 
			
		||||
                    $original = $server->image;
 | 
			
		||||
                    $server->forceFill(['image' => $new])->saveOrFail();
 | 
			
		||||
 | 
			
		||||
                    if ($original !== $server->image) {
 | 
			
		||||
                        Activity::event('server:startup.image')
 | 
			
		||||
                            ->property(['old' => $original, 'new' => $new])
 | 
			
		||||
                            ->log();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $powerRepository->setServer($server)->send('restart');
 | 
			
		||||
 | 
			
		||||
                    Notification::make()
 | 
			
		||||
                        ->title('Docker image updated')
 | 
			
		||||
                        ->body('Server will restart now.')
 | 
			
		||||
                        ->success()
 | 
			
		||||
                        ->send();
 | 
			
		||||
                } catch (Exception $exception) {
 | 
			
		||||
                    Notification::make()
 | 
			
		||||
                        ->title('Could not update docker image')
 | 
			
		||||
                        ->body($exception->getMessage())
 | 
			
		||||
                        ->danger()
 | 
			
		||||
                        ->send();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function register(Application $app): self
 | 
			
		||||
    {
 | 
			
		||||
        return new self($app);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										71
									
								
								app/Extensions/Features/MinecraftEula.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								app/Extensions/Features/MinecraftEula.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Features;
 | 
			
		||||
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Repositories\Daemon\DaemonFileRepository;
 | 
			
		||||
use App\Repositories\Daemon\DaemonPowerRepository;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Filament\Actions\Action;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
use Illuminate\Support\Facades\Blade;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
 | 
			
		||||
class MinecraftEula extends FeatureProvider
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(protected Application $app)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return array<string> */
 | 
			
		||||
    public function getListeners(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'you need to agree to the eula in order to run the server',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'eula';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAction(): Action
 | 
			
		||||
    {
 | 
			
		||||
        return Action::make($this->getId())
 | 
			
		||||
            ->requiresConfirmation()
 | 
			
		||||
            ->modalHeading('Minecraft EULA')
 | 
			
		||||
            ->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
 | 
			
		||||
            ->modalSubmitActionLabel('I Accept')
 | 
			
		||||
            ->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
 | 
			
		||||
                try {
 | 
			
		||||
                    /** @var Server $server */
 | 
			
		||||
                    $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
                    $fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
 | 
			
		||||
 | 
			
		||||
                    $powerRepository->setServer($server)->send('restart');
 | 
			
		||||
 | 
			
		||||
                    Notification::make()
 | 
			
		||||
                        ->title('Minecraft EULA accepted')
 | 
			
		||||
                        ->body('Server will restart now.')
 | 
			
		||||
                        ->success()
 | 
			
		||||
                        ->send();
 | 
			
		||||
                } catch (Exception $exception) {
 | 
			
		||||
                    Notification::make()
 | 
			
		||||
                        ->title('Could not accept Minecraft EULA')
 | 
			
		||||
                        ->body($exception->getMessage())
 | 
			
		||||
                        ->danger()
 | 
			
		||||
                        ->send();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function register(Application $app): self
 | 
			
		||||
    {
 | 
			
		||||
        return new self($app);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								app/Extensions/Features/PIDLimit.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								app/Extensions/Features/PIDLimit.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Features;
 | 
			
		||||
 | 
			
		||||
use Filament\Actions\Action;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
use Illuminate\Support\Facades\Blade;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
 | 
			
		||||
class PIDLimit extends FeatureProvider
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(protected Application $app)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return array<string> */
 | 
			
		||||
    public function getListeners(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'pthread_create failed',
 | 
			
		||||
            'failed to create thread',
 | 
			
		||||
            'unable to create thread',
 | 
			
		||||
            'unable to create native thread',
 | 
			
		||||
            'unable to create new native thread',
 | 
			
		||||
            'exception in thread "craft async scheduler management thread"',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'pid_limit';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAction(): Action
 | 
			
		||||
    {
 | 
			
		||||
        return Action::make($this->getId())
 | 
			
		||||
            ->requiresConfirmation()
 | 
			
		||||
            ->icon('tabler-alert-triangle')
 | 
			
		||||
            ->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
 | 
			
		||||
            ->modalDescription(new HtmlString(Blade::render(
 | 
			
		||||
                auth()->user()->isAdmin() ? <<<'HTML'
 | 
			
		||||
                    <p>
 | 
			
		||||
                        This server has reached the maximum process or memory limit.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p class="mt-4">
 | 
			
		||||
                        Increasing <code>container_pid_limit</code> in the wings
 | 
			
		||||
                        configuration, <code>config.yml</code>, might help resolve
 | 
			
		||||
                        this issue.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p class="mt-4">
 | 
			
		||||
                        <b>Note: Wings must be restarted for the configuration file changes to take effect</b>
 | 
			
		||||
                    </p>
 | 
			
		||||
                HTML
 | 
			
		||||
                :
 | 
			
		||||
                <<<'HTML'
 | 
			
		||||
                    <p>
 | 
			
		||||
                        This server is attempting to use more resources than allocated. Please contact the administrator
 | 
			
		||||
                        and give them the error below.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p class="mt-4">
 | 
			
		||||
                        <code>
 | 
			
		||||
                            pthread_create failed, Possibly out of memory or process/resource limits reached
 | 
			
		||||
                        </code>
 | 
			
		||||
                    </p>
 | 
			
		||||
                HTML
 | 
			
		||||
            )))
 | 
			
		||||
            ->modalCancelActionLabel('Close')
 | 
			
		||||
            ->action(fn () => null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function register(Application $app): self
 | 
			
		||||
    {
 | 
			
		||||
        return new self($app);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										64
									
								
								app/Extensions/Features/SteamDiskSpace.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app/Extensions/Features/SteamDiskSpace.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Extensions\Features;
 | 
			
		||||
 | 
			
		||||
use Filament\Actions\Action;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
use Illuminate\Support\Facades\Blade;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
 | 
			
		||||
class SteamDiskSpace extends FeatureProvider
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(protected Application $app)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($app);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return array<string> */
 | 
			
		||||
    public function getListeners(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'steamcmd needs 250mb of free disk space to update',
 | 
			
		||||
            '0x202 after update job',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'steam_disk_space';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAction(): Action
 | 
			
		||||
    {
 | 
			
		||||
        return Action::make($this->getId())
 | 
			
		||||
            ->requiresConfirmation()
 | 
			
		||||
            ->modalHeading('Out of available disk space...')
 | 
			
		||||
            ->modalDescription(new HtmlString(Blade::render(
 | 
			
		||||
                auth()->user()->isAdmin() ? <<<'HTML'
 | 
			
		||||
                    <p>
 | 
			
		||||
                        This server has run out of available disk space and cannot complete the install or update
 | 
			
		||||
                        process.
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p class="mt-4">
 | 
			
		||||
                        Ensure the machine has enough disk space by typing{' '}
 | 
			
		||||
                        <code class="rounded py-1 px-2">df -h</code> on the machine hosting
 | 
			
		||||
                        this server. Delete files or increase the available disk space to resolve the issue.
 | 
			
		||||
                    </p>
 | 
			
		||||
                HTML
 | 
			
		||||
                :
 | 
			
		||||
                <<<'HTML'
 | 
			
		||||
                    <p>
 | 
			
		||||
                        This server has run out of available disk space and cannot complete the install or update
 | 
			
		||||
                        process. Please get in touch with the administrator(s) and inform them of disk space issues.
 | 
			
		||||
                    </p>
 | 
			
		||||
                HTML
 | 
			
		||||
            )))
 | 
			
		||||
            ->modalCancelActionLabel('Close')
 | 
			
		||||
            ->action(fn () => null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function register(Application $app): self
 | 
			
		||||
    {
 | 
			
		||||
        return new self($app);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -6,8 +6,8 @@ use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Components\Wizard\Step;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
use Illuminate\Support\Facades\Blade;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use SocialiteProviders\Discord\Provider;
 | 
			
		||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
 | 
			
		||||
 | 
			
		||||
@ -34,15 +34,15 @@ final class DiscordProvider extends OAuthProvider
 | 
			
		||||
            Step::make('Register new Discord OAuth App')
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Placeholder::make('')
 | 
			
		||||
                        ->content(new HtmlString('<p>Visit the <u><a href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</a></u> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b>, you will need them in the final step.</p>')),
 | 
			
		||||
                        ->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))),
 | 
			
		||||
                    Placeholder::make('')
 | 
			
		||||
                        ->content(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
 | 
			
		||||
                    TextInput::make('_noenv_callback')
 | 
			
		||||
                        ->label('Redirect URL')
 | 
			
		||||
                        ->dehydrated()
 | 
			
		||||
                        ->disabled()
 | 
			
		||||
                        ->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
 | 
			
		||||
                        ->formatStateUsing(fn () => config('app.url') . (Str::endsWith(config('app.url'), '/') ? '' : '/') . 'auth/oauth/callback/discord'),
 | 
			
		||||
                        ->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
 | 
			
		||||
                        ->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
 | 
			
		||||
                ]),
 | 
			
		||||
        ], parent::getSetupSteps());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,8 @@ use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Components\Wizard\Step;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
use Illuminate\Support\Facades\Blade;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
 | 
			
		||||
 | 
			
		||||
final class GithubProvider extends OAuthProvider
 | 
			
		||||
@ -28,13 +28,13 @@ final class GithubProvider extends OAuthProvider
 | 
			
		||||
            Step::make('Register new Github OAuth App')
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Placeholder::make('')
 | 
			
		||||
                        ->content(new HtmlString('<p>Visit the <u><a href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</a></u>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>')),
 | 
			
		||||
                        ->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
 | 
			
		||||
                    TextInput::make('_noenv_callback')
 | 
			
		||||
                        ->label('Authorization callback URL')
 | 
			
		||||
                        ->dehydrated()
 | 
			
		||||
                        ->disabled()
 | 
			
		||||
                        ->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
 | 
			
		||||
                        ->default(fn () => config('app.url') . (Str::endsWith(config('app.url'), '/') ? '' : '/') . 'auth/oauth/callback/github'),
 | 
			
		||||
                        ->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
 | 
			
		||||
                        ->default(fn () => url('/auth/oauth/callback/github')),
 | 
			
		||||
                    Placeholder::make('')
 | 
			
		||||
                        ->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
 | 
			
		||||
                ]),
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@ use Filament\Forms\Components\Wizard\Step;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
use Illuminate\Support\Facades\Blade;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
 | 
			
		||||
 | 
			
		||||
final class GitlabProvider extends OAuthProvider
 | 
			
		||||
@ -54,8 +53,8 @@ final class GitlabProvider extends OAuthProvider
 | 
			
		||||
                        ->label('Redirect URI')
 | 
			
		||||
                        ->dehydrated()
 | 
			
		||||
                        ->disabled()
 | 
			
		||||
                        ->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
 | 
			
		||||
                        ->default(fn () => config('app.url') . (Str::endsWith(config('app.url'), '/') ? '' : '/') . 'auth/oauth/callback/gitlab'),
 | 
			
		||||
                        ->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
 | 
			
		||||
                        ->default(fn () => url('/auth/oauth/callback/gitlab')),
 | 
			
		||||
                ]),
 | 
			
		||||
        ], parent::getSetupSteps());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Components\Wizard\Step;
 | 
			
		||||
use Illuminate\Foundation\Application;
 | 
			
		||||
use Illuminate\Support\Facades\Blade;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
use SocialiteProviders\Steam\Provider;
 | 
			
		||||
 | 
			
		||||
@ -58,7 +59,7 @@ final class SteamProvider extends OAuthProvider
 | 
			
		||||
            Step::make('Create API Key')
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Placeholder::make('')
 | 
			
		||||
                        ->content(new HtmlString('Visit <u><a href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</a></u> to generate an API key.')),
 | 
			
		||||
                        ->content(new HtmlString(Blade::render('Visit <x-filament::link href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</x-filament::link> to generate an API key.'))),
 | 
			
		||||
                ]),
 | 
			
		||||
        ], parent::getSetupSteps());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -2,36 +2,13 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
 | 
			
		||||
use App\Filament\Admin\Resources\NodeResource\Pages\ListNodes;
 | 
			
		||||
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;
 | 
			
		||||
use Filament\Pages\Dashboard as BaseDashboard;
 | 
			
		||||
 | 
			
		||||
class Dashboard extends Page
 | 
			
		||||
class Dashboard extends BaseDashboard
 | 
			
		||||
{
 | 
			
		||||
    protected static ?string $navigationIcon = 'tabler-layout-dashboard';
 | 
			
		||||
 | 
			
		||||
    protected static string $view = 'filament.pages.dashboard';
 | 
			
		||||
 | 
			
		||||
    protected ?string $heading = '';
 | 
			
		||||
 | 
			
		||||
    public function getTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.title');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationLabel(): string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.title');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected static ?string $slug = '/';
 | 
			
		||||
 | 
			
		||||
    private SoftwareVersionService $softwareVersionService;
 | 
			
		||||
 | 
			
		||||
    public function mount(SoftwareVersionService $softwareVersionService): void
 | 
			
		||||
@ -39,51 +16,18 @@ class Dashboard extends Page
 | 
			
		||||
        $this->softwareVersionService = $softwareVersionService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getViewData(): array
 | 
			
		||||
    public function getColumns(): int
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'inDevelopment' => config('app.version') === 'canary',
 | 
			
		||||
            'version' => $this->softwareVersionService->currentPanelVersion(),
 | 
			
		||||
            'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
 | 
			
		||||
            'isLatest' => $this->softwareVersionService->isLatestPanel(),
 | 
			
		||||
            'eggsCount' => Egg::query()->count(),
 | 
			
		||||
            'nodesList' => ListNodes::getUrl(),
 | 
			
		||||
            'nodesCount' => Node::query()->count(),
 | 
			
		||||
            'serversCount' => Server::query()->count(),
 | 
			
		||||
            'usersCount' => User::query()->count(),
 | 
			
		||||
        return 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
            'devActions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
 | 
			
		||||
                    ->icon('tabler-brand-github')
 | 
			
		||||
                    ->url('https://github.com/pelican-dev/panel/issues', true),
 | 
			
		||||
            ],
 | 
			
		||||
            'updateActions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-update-available.heading'))
 | 
			
		||||
                    ->icon('tabler-clipboard-text')
 | 
			
		||||
                    ->url('https://pelican.dev/docs/panel/update', true)
 | 
			
		||||
                    ->color('warning'),
 | 
			
		||||
            ],
 | 
			
		||||
            'nodeActions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-first-node.button_label'))
 | 
			
		||||
                    ->icon('tabler-server-2')
 | 
			
		||||
                    ->url(CreateNode::getUrl()),
 | 
			
		||||
            ],
 | 
			
		||||
            'supportActions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-support.button_donate'))
 | 
			
		||||
                    ->icon('tabler-cash')
 | 
			
		||||
                    ->url('https://pelican.dev/donate', true)
 | 
			
		||||
                    ->color('success'),
 | 
			
		||||
            ],
 | 
			
		||||
            'helpActions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-help.button_docs'))
 | 
			
		||||
                    ->icon('tabler-speedboat')
 | 
			
		||||
                    ->url('https://pelican.dev/docs', true),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    public function getHeading(): string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.heading');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getSubheading(): string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.version', ['version' => $this->softwareVersionService->currentPanelVersion()]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Extensions\Avatar\AvatarProvider;
 | 
			
		||||
use App\Extensions\Captcha\Providers\CaptchaProvider;
 | 
			
		||||
use App\Extensions\OAuth\Providers\OAuthProvider;
 | 
			
		||||
use App\Models\Backup;
 | 
			
		||||
@ -12,6 +13,7 @@ use Filament\Actions\Action;
 | 
			
		||||
use Filament\Forms\Components\Actions;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action as FormAction;
 | 
			
		||||
use Filament\Forms\Components\Component;
 | 
			
		||||
use Filament\Forms\Components\FileUpload;
 | 
			
		||||
use Filament\Forms\Components\Group;
 | 
			
		||||
use Filament\Forms\Components\Hidden;
 | 
			
		||||
use Filament\Forms\Components\Section;
 | 
			
		||||
@ -32,6 +34,7 @@ use Filament\Pages\Concerns\InteractsWithHeaderActions;
 | 
			
		||||
use Filament\Pages\Page;
 | 
			
		||||
use Filament\Support\Enums\MaxWidth;
 | 
			
		||||
use Illuminate\Http\Client\Factory;
 | 
			
		||||
use Illuminate\Support\Arr;
 | 
			
		||||
use Illuminate\Support\Facades\Artisan;
 | 
			
		||||
use Illuminate\Support\Facades\Notification as MailNotification;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
@ -134,26 +137,50 @@ class Settings extends Page implements HasForms
 | 
			
		||||
                        ->default(env('APP_FAVICON', '/pelican.ico'))
 | 
			
		||||
                        ->placeholder('/pelican.ico'),
 | 
			
		||||
                ]),
 | 
			
		||||
            Toggle::make('APP_DEBUG')
 | 
			
		||||
                ->label(trans('admin/setting.general.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(trans('admin/setting.general.navigation'))
 | 
			
		||||
                ->inline()
 | 
			
		||||
                ->options([
 | 
			
		||||
                    false => trans('admin/setting.general.sidebar'),
 | 
			
		||||
                    true => trans('admin/setting.general.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'))),
 | 
			
		||||
            Group::make()
 | 
			
		||||
                ->columns(2)
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Toggle::make('APP_DEBUG')
 | 
			
		||||
                        ->label(trans('admin/setting.general.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(trans('admin/setting.general.navigation'))
 | 
			
		||||
                        ->inline()
 | 
			
		||||
                        ->options([
 | 
			
		||||
                            false => trans('admin/setting.general.sidebar'),
 | 
			
		||||
                            true => trans('admin/setting.general.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'))),
 | 
			
		||||
                ]),
 | 
			
		||||
            Group::make()
 | 
			
		||||
                ->columns(2)
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Select::make('FILAMENT_AVATAR_PROVIDER')
 | 
			
		||||
                        ->label(trans('admin/setting.general.avatar_provider'))
 | 
			
		||||
                        ->native(false)
 | 
			
		||||
                        ->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()]))
 | 
			
		||||
                        ->selectablePlaceholder(false)
 | 
			
		||||
                        ->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
 | 
			
		||||
                    Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
 | 
			
		||||
                        ->label(trans('admin/setting.general.uploadable_avatars'))
 | 
			
		||||
                        ->inline(false)
 | 
			
		||||
                        ->onIcon('tabler-check')
 | 
			
		||||
                        ->offIcon('tabler-x')
 | 
			
		||||
                        ->onColor('success')
 | 
			
		||||
                        ->offColor('danger')
 | 
			
		||||
                        ->formatStateUsing(fn ($state) => (bool) $state)
 | 
			
		||||
                        ->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_UPLOADABLE_AVATARS', (bool) $state))
 | 
			
		||||
                        ->default(env('FILAMENT_UPLOADABLE_AVATARS', config('panel.filament.uploadable-avatars'))),
 | 
			
		||||
                ]),
 | 
			
		||||
            ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
 | 
			
		||||
                ->label(trans('admin/setting.general.unit_prefix'))
 | 
			
		||||
                ->inline()
 | 
			
		||||
@ -175,12 +202,18 @@ class Settings extends Page implements HasForms
 | 
			
		||||
                ->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'))),
 | 
			
		||||
            Select::make('FILAMENT_WIDTH')
 | 
			
		||||
                ->label(trans('admin/setting.general.display_width'))
 | 
			
		||||
                ->native(false)
 | 
			
		||||
                ->options(MaxWidth::class)
 | 
			
		||||
                ->selectablePlaceholder(false)
 | 
			
		||||
                ->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
 | 
			
		||||
            TagsInput::make('TRUSTED_PROXIES')
 | 
			
		||||
                ->label(trans('admin/setting.general.trusted_proxies'))
 | 
			
		||||
                ->separator()
 | 
			
		||||
                ->splitKeys(['Tab', ' '])
 | 
			
		||||
                ->placeholder(trans('admin/setting.general.trusted_proxies_help'))
 | 
			
		||||
                ->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
 | 
			
		||||
                ->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies')))))
 | 
			
		||||
                ->hintActions([
 | 
			
		||||
                    FormAction::make('clear')
 | 
			
		||||
                        ->label(trans('admin/setting.general.clear'))
 | 
			
		||||
@ -215,12 +248,6 @@ class Settings extends Page implements HasForms
 | 
			
		||||
                            $set('TRUSTED_PROXIES', $ips->values()->all());
 | 
			
		||||
                        }),
 | 
			
		||||
                ]),
 | 
			
		||||
            Select::make('FILAMENT_WIDTH')
 | 
			
		||||
                ->label(trans('admin/setting.general.display_width'))
 | 
			
		||||
                ->native(false)
 | 
			
		||||
                ->options(MaxWidth::class)
 | 
			
		||||
                ->selectablePlaceholder(false)
 | 
			
		||||
                ->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -702,10 +729,17 @@ class Settings extends Page implements HasForms
 | 
			
		||||
                        ->onColor('success')
 | 
			
		||||
                        ->offColor('danger')
 | 
			
		||||
                        ->live()
 | 
			
		||||
                        ->columnSpanFull()
 | 
			
		||||
                        ->columnSpan(1)
 | 
			
		||||
                        ->formatStateUsing(fn ($state): bool => (bool) $state)
 | 
			
		||||
                        ->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
 | 
			
		||||
                        ->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
 | 
			
		||||
                    FileUpload::make('ConsoleFonts')
 | 
			
		||||
                        ->hint(trans('admin/setting.misc.server.console_font_hint'))
 | 
			
		||||
                        ->label(trans('admin/setting.misc.server.console_font_upload'))
 | 
			
		||||
                        ->directory('fonts')
 | 
			
		||||
                        ->columnSpan(1)
 | 
			
		||||
                        ->maxFiles(1)
 | 
			
		||||
                        ->preserveFilenames(),
 | 
			
		||||
                ]),
 | 
			
		||||
            Section::make(trans('admin/setting.misc.webhook.title'))
 | 
			
		||||
                ->description(trans('admin/setting.misc.webhook.helper'))
 | 
			
		||||
@ -734,6 +768,7 @@ class Settings extends Page implements HasForms
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $data = $this->form->getState();
 | 
			
		||||
            unset($data['ConsoleFonts']);
 | 
			
		||||
 | 
			
		||||
            // 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);
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ use Filament\Tables\Actions\EditAction;
 | 
			
		||||
use Filament\Tables\Actions\ViewAction;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
 | 
			
		||||
class DatabaseHostResource extends Resource
 | 
			
		||||
{
 | 
			
		||||
@ -27,7 +28,7 @@ class DatabaseHostResource extends Resource
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationBadge(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return static::getModel()::count() ?: null;
 | 
			
		||||
        return (string) static::getEloquentQuery()->count() ?: null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationLabel(): string
 | 
			
		||||
@ -144,7 +145,7 @@ class DatabaseHostResource extends Resource
 | 
			
		||||
                            ->preload()
 | 
			
		||||
                            ->helperText(trans('admin/databasehost.linked_nodes_help'))
 | 
			
		||||
                            ->label(trans('admin/databasehost.linked_nodes'))
 | 
			
		||||
                            ->relationship('nodes', 'name'),
 | 
			
		||||
                            ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
 | 
			
		||||
                    ]),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
@ -158,4 +159,13 @@ class DatabaseHostResource extends Resource
 | 
			
		||||
            'edit' => Pages\EditDatabaseHost::route('/{record}/edit'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getEloquentQuery(): Builder
 | 
			
		||||
    {
 | 
			
		||||
        $query = parent::getEloquentQuery();
 | 
			
		||||
 | 
			
		||||
        return $query->whereHas('nodes', function (Builder $query) {
 | 
			
		||||
            $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
 | 
			
		||||
        })->orDoesntHave('nodes');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,14 +4,30 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Filament\Admin\Resources\DatabaseHostResource;
 | 
			
		||||
use App\Services\Databases\Hosts\HostCreationService;
 | 
			
		||||
use Filament\Forms\Components\Fieldset;
 | 
			
		||||
use Filament\Forms\Components\Hidden;
 | 
			
		||||
use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\Select;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Components\Toggle;
 | 
			
		||||
use Filament\Forms\Components\Wizard\Step;
 | 
			
		||||
use Filament\Forms\Get;
 | 
			
		||||
use Filament\Forms\Set;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Filament\Resources\Pages\CreateRecord;
 | 
			
		||||
use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard;
 | 
			
		||||
use Filament\Support\Exceptions\Halt;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use PDOException;
 | 
			
		||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
 | 
			
		||||
 | 
			
		||||
class CreateDatabaseHost extends CreateRecord
 | 
			
		||||
{
 | 
			
		||||
    use HasWizard;
 | 
			
		||||
 | 
			
		||||
    protected static string $resource = DatabaseHostResource::class;
 | 
			
		||||
 | 
			
		||||
    protected static bool $canCreateAnother = false;
 | 
			
		||||
@ -23,18 +39,118 @@ class CreateDatabaseHost extends CreateRecord
 | 
			
		||||
        $this->service = $service;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getHeaderActions(): array
 | 
			
		||||
    /** @return Step[] */
 | 
			
		||||
    public function getSteps(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            $this->getCreateFormAction()->formId('form'),
 | 
			
		||||
            Step::make(trans('admin/databasehost.setup.preparations'))
 | 
			
		||||
                ->columns()
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Placeholder::make('')
 | 
			
		||||
                        ->content(trans('admin/databasehost.setup.note')),
 | 
			
		||||
                    Toggle::make('different_server')
 | 
			
		||||
                        ->label(new HtmlString(trans('admin/databasehost.setup.different_server')))
 | 
			
		||||
                        ->dehydrated(false)
 | 
			
		||||
                        ->live()
 | 
			
		||||
                        ->columnSpanFull()
 | 
			
		||||
                        ->afterStateUpdated(fn ($state, Set $set) => $state ? $set('panel_ip', gethostbyname(str(config('app.url'))->replace(['http:', 'https:', '/'], ''))) : '127.0.0.1'),
 | 
			
		||||
                    Hidden::make('panel_ip')
 | 
			
		||||
                        ->default('127.0.0.1')
 | 
			
		||||
                        ->dehydrated(false),
 | 
			
		||||
                    TextInput::make('username')
 | 
			
		||||
                        ->label(trans('admin/databasehost.username'))
 | 
			
		||||
                        ->helperText(trans('admin/databasehost.username_help'))
 | 
			
		||||
                        ->required()
 | 
			
		||||
                        ->default('pelicanuser')
 | 
			
		||||
                        ->maxLength(255),
 | 
			
		||||
                    TextInput::make('password')
 | 
			
		||||
                        ->label(trans('admin/databasehost.password'))
 | 
			
		||||
                        ->helperText(trans('admin/databasehost.password_help'))
 | 
			
		||||
                        ->required()
 | 
			
		||||
                        ->default(Str::password(16))
 | 
			
		||||
                        ->password()
 | 
			
		||||
                        ->revealable()
 | 
			
		||||
                        ->maxLength(255),
 | 
			
		||||
                ])
 | 
			
		||||
                ->afterValidation(function (Get $get, Set $set) {
 | 
			
		||||
                    $set('create_user', "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';");
 | 
			
		||||
                    $set('assign_permissions', "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;");
 | 
			
		||||
                }),
 | 
			
		||||
            Step::make(trans('admin/databasehost.setup.database_setup'))
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Fieldset::make(trans('admin/databasehost.setup.database_user'))
 | 
			
		||||
                        ->schema([
 | 
			
		||||
                            Placeholder::make('')
 | 
			
		||||
                                ->content(new HtmlString(trans('admin/databasehost.setup.cli_login')))
 | 
			
		||||
                                ->columnSpanFull(),
 | 
			
		||||
                            TextInput::make('create_user')
 | 
			
		||||
                                ->label(trans('admin/databasehost.setup.command_create_user'))
 | 
			
		||||
                                ->default(fn (Get $get) => "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';")
 | 
			
		||||
                                ->disabled()
 | 
			
		||||
                                ->dehydrated(false)
 | 
			
		||||
                                ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
 | 
			
		||||
                                ->columnSpanFull(),
 | 
			
		||||
                            TextInput::make('assign_permissions')
 | 
			
		||||
                                ->label(trans('admin/databasehost.setup.command_assign_permissions'))
 | 
			
		||||
                                ->default(fn (Get $get) => "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;")
 | 
			
		||||
                                ->disabled()
 | 
			
		||||
                                ->dehydrated(false)
 | 
			
		||||
                                ->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
 | 
			
		||||
                                ->columnSpanFull(),
 | 
			
		||||
                            Placeholder::make('')
 | 
			
		||||
                                ->content(new HtmlString(trans('admin/databasehost.setup.cli_exit')))
 | 
			
		||||
                                ->columnSpanFull(),
 | 
			
		||||
                        ]),
 | 
			
		||||
                    Fieldset::make(trans('admin/databasehost.setup.external_access'))
 | 
			
		||||
                        ->schema([
 | 
			
		||||
                            Placeholder::make('')
 | 
			
		||||
                                ->content(new HtmlString(trans('admin/databasehost.setup.allow_external_access')))
 | 
			
		||||
                                ->columnSpanFull(),
 | 
			
		||||
                        ]),
 | 
			
		||||
                ]),
 | 
			
		||||
            Step::make(trans('admin/databasehost.setup.panel_setup'))
 | 
			
		||||
                ->columns([
 | 
			
		||||
                    'default' => 2,
 | 
			
		||||
                    'lg' => 3,
 | 
			
		||||
                ])
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    TextInput::make('host')
 | 
			
		||||
                        ->columnSpan(2)
 | 
			
		||||
                        ->label(trans('admin/databasehost.host'))
 | 
			
		||||
                        ->helperText(trans('admin/databasehost.host_help'))
 | 
			
		||||
                        ->required()
 | 
			
		||||
                        ->live(onBlur: true)
 | 
			
		||||
                        ->afterStateUpdated(fn ($state, Set $set) => $set('name', $state))
 | 
			
		||||
                        ->maxLength(255),
 | 
			
		||||
                    TextInput::make('port')
 | 
			
		||||
                        ->label(trans('admin/databasehost.port'))
 | 
			
		||||
                        ->helperText(trans('admin/databasehost.port_help'))
 | 
			
		||||
                        ->required()
 | 
			
		||||
                        ->numeric()
 | 
			
		||||
                        ->default(3306)
 | 
			
		||||
                        ->minValue(0)
 | 
			
		||||
                        ->maxValue(65535),
 | 
			
		||||
                    TextInput::make('max_databases')
 | 
			
		||||
                        ->label(trans('admin/databasehost.max_database'))
 | 
			
		||||
                        ->helpertext(trans('admin/databasehost.max_databases_help'))
 | 
			
		||||
                        ->placeholder(trans('admin/databasehost.unlimited'))
 | 
			
		||||
                        ->numeric(),
 | 
			
		||||
                    TextInput::make('name')
 | 
			
		||||
                        ->label(trans('admin/databasehost.display_name'))
 | 
			
		||||
                        ->helperText(trans('admin/databasehost.display_name_help'))
 | 
			
		||||
                        ->required()
 | 
			
		||||
                        ->maxLength(60),
 | 
			
		||||
                    Select::make('node_ids')
 | 
			
		||||
                        ->multiple()
 | 
			
		||||
                        ->searchable()
 | 
			
		||||
                        ->preload()
 | 
			
		||||
                        ->helperText(trans('admin/databasehost.linked_nodes_help'))
 | 
			
		||||
                        ->label(trans('admin/databasehost.linked_nodes'))
 | 
			
		||||
                        ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'))),
 | 
			
		||||
                ]),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getFormActions(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function handleRecordCreation(array $data): Model
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ class EggResource extends Resource
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationGroup(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.server');
 | 
			
		||||
        return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationLabel(): string
 | 
			
		||||
 | 
			
		||||
@ -243,6 +243,7 @@ class CreateEgg extends CreateRecord
 | 
			
		||||
                                ->default('ghcr.io/pelican-eggs/installers:debian'),
 | 
			
		||||
                            Select::make('script_entry')
 | 
			
		||||
                                ->label(trans('admin/egg.script_entry'))
 | 
			
		||||
                                ->native(false)
 | 
			
		||||
                                ->selectablePlaceholder(false)
 | 
			
		||||
                                ->default('bash')
 | 
			
		||||
                                ->options(['bash', 'ash', '/bin/bash'])
 | 
			
		||||
 | 
			
		||||
@ -235,6 +235,7 @@ class EditEgg extends EditRecord
 | 
			
		||||
                                ->placeholder('ghcr.io/pelican-eggs/installers:debian'),
 | 
			
		||||
                            Select::make('script_entry')
 | 
			
		||||
                                ->label(trans('admin/egg.script_entry'))
 | 
			
		||||
                                ->native(false)
 | 
			
		||||
                                ->selectablePlaceholder(false)
 | 
			
		||||
                                ->options(['bash', 'ash', '/bin/bash'])
 | 
			
		||||
                                ->required(),
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ use Filament\Tables\Actions\EditAction;
 | 
			
		||||
use Filament\Tables\Actions\ViewAction;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
 | 
			
		||||
class MountResource extends Resource
 | 
			
		||||
{
 | 
			
		||||
@ -44,7 +45,7 @@ class MountResource extends Resource
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationBadge(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return static::getModel()::count() ?: null;
 | 
			
		||||
        return (string) static::getEloquentQuery()->count() ?: null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationGroup(): ?string
 | 
			
		||||
@ -147,7 +148,7 @@ class MountResource extends Resource
 | 
			
		||||
                            ->preload(),
 | 
			
		||||
                        Select::make('nodes')->multiple()
 | 
			
		||||
                            ->label(trans('admin/mount.nodes'))
 | 
			
		||||
                            ->relationship('nodes', 'name')
 | 
			
		||||
                            ->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id')))
 | 
			
		||||
                            ->searchable(['name', 'fqdn'])
 | 
			
		||||
                            ->preload(),
 | 
			
		||||
                    ]),
 | 
			
		||||
@ -170,4 +171,13 @@ class MountResource extends Resource
 | 
			
		||||
            'edit' => Pages\EditMount::route('/{record}/edit'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getEloquentQuery(): Builder
 | 
			
		||||
    {
 | 
			
		||||
        $query = parent::getEloquentQuery();
 | 
			
		||||
 | 
			
		||||
        return $query->whereHas('nodes', function (Builder $query) {
 | 
			
		||||
            $query->whereIn('nodes.id', auth()->user()->accessibleNodes()->pluck('id'));
 | 
			
		||||
        })->orDoesntHave('nodes');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ use App\Filament\Admin\Resources\NodeResource\Pages;
 | 
			
		||||
use App\Filament\Admin\Resources\NodeResource\RelationManagers;
 | 
			
		||||
use App\Models\Node;
 | 
			
		||||
use Filament\Resources\Resource;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
 | 
			
		||||
class NodeResource extends Resource
 | 
			
		||||
{
 | 
			
		||||
@ -32,12 +33,12 @@ class NodeResource extends Resource
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationGroup(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.server');
 | 
			
		||||
        return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationBadge(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return static::getModel()::count() ?: null;
 | 
			
		||||
        return (string) static::getEloquentQuery()->count() ?: null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getRelations(): array
 | 
			
		||||
@ -56,4 +57,11 @@ class NodeResource extends Resource
 | 
			
		||||
            'edit' => Pages\EditNode::route('/{record}/edit'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getEloquentQuery(): Builder
 | 
			
		||||
    {
 | 
			
		||||
        $query = parent::getEloquentQuery();
 | 
			
		||||
 | 
			
		||||
        return $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,9 +3,11 @@
 | 
			
		||||
namespace App\Filament\Admin\Resources\NodeResource\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Filament\Admin\Resources\NodeResource;
 | 
			
		||||
use App\Models\Node;
 | 
			
		||||
use Filament\Forms;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action;
 | 
			
		||||
use Filament\Forms\Components\Grid;
 | 
			
		||||
use Filament\Forms\Components\Hidden;
 | 
			
		||||
use Filament\Forms\Components\TagsInput;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Components\ToggleButtons;
 | 
			
		||||
@ -44,7 +46,8 @@ class CreateNode extends CreateRecord
 | 
			
		||||
                                ->required()
 | 
			
		||||
                                ->autofocus()
 | 
			
		||||
                                ->live(debounce: 1500)
 | 
			
		||||
                                ->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
 | 
			
		||||
                                ->rules(Node::getRulesForField('fqdn'))
 | 
			
		||||
                                ->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
 | 
			
		||||
                                ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
 | 
			
		||||
                                ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
 | 
			
		||||
                                ->helperText(function ($state) {
 | 
			
		||||
@ -147,14 +150,15 @@ class CreateNode extends CreateRecord
 | 
			
		||||
                                ->required()
 | 
			
		||||
                                ->maxLength(100),
 | 
			
		||||
 | 
			
		||||
                            ToggleButtons::make('scheme')
 | 
			
		||||
                            Hidden::make('scheme')
 | 
			
		||||
                                ->default(fn () => request()->isSecure() ? 'https' : 'http'),
 | 
			
		||||
 | 
			
		||||
                            Hidden::make('behind_proxy')
 | 
			
		||||
                                ->default(false),
 | 
			
		||||
 | 
			
		||||
                            ToggleButtons::make('connection')
 | 
			
		||||
                                ->label(trans('admin/node.ssl'))
 | 
			
		||||
                                ->columnSpan([
 | 
			
		||||
                                    'default' => 1,
 | 
			
		||||
                                    'sm' => 1,
 | 
			
		||||
                                    'md' => 1,
 | 
			
		||||
                                    'lg' => 1,
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->columnSpan(1)
 | 
			
		||||
                                ->inline()
 | 
			
		||||
                                ->helperText(function (Get $get) {
 | 
			
		||||
                                    if (request()->isSecure()) {
 | 
			
		||||
@ -167,20 +171,29 @@ class CreateNode extends CreateRecord
 | 
			
		||||
 | 
			
		||||
                                    return '';
 | 
			
		||||
                                })
 | 
			
		||||
                                ->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
 | 
			
		||||
                                ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
 | 
			
		||||
                                ->options([
 | 
			
		||||
                                    'http' => 'HTTP',
 | 
			
		||||
                                    'https' => 'HTTPS (SSL)',
 | 
			
		||||
                                    'https_proxy' => 'HTTPS with (reverse) proxy',
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->colors([
 | 
			
		||||
                                    'http' => 'warning',
 | 
			
		||||
                                    'https' => 'success',
 | 
			
		||||
                                    'https_proxy' => 'success',
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->icons([
 | 
			
		||||
                                    'http' => 'tabler-lock-open-off',
 | 
			
		||||
                                    'https' => 'tabler-lock',
 | 
			
		||||
                                    'https_proxy' => 'tabler-shield-lock',
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->default(fn () => request()->isSecure() ? 'https' : 'http'),
 | 
			
		||||
                                ->default(fn () => request()->isSecure() ? 'https' : 'http')
 | 
			
		||||
                                ->live()
 | 
			
		||||
                                ->dehydrated(false)
 | 
			
		||||
                                ->afterStateUpdated(function ($state, Set $set) {
 | 
			
		||||
                                    $set('scheme', $state === 'http' ? 'http' : 'https');
 | 
			
		||||
                                    $set('behind_proxy', $state === 'https_proxy');
 | 
			
		||||
                                }),
 | 
			
		||||
                        ]),
 | 
			
		||||
                    Step::make('advanced')
 | 
			
		||||
                        ->label(trans('admin/node.tabs.advanced_settings'))
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ namespace App\Filament\Admin\Resources\NodeResource\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Filament\Admin\Resources\NodeResource;
 | 
			
		||||
use App\Models\Node;
 | 
			
		||||
use App\Repositories\Daemon\DaemonConfigurationRepository;
 | 
			
		||||
use App\Services\Helpers\SoftwareVersionService;
 | 
			
		||||
use App\Services\Nodes\NodeAutoDeployService;
 | 
			
		||||
use App\Services\Nodes\NodeUpdateService;
 | 
			
		||||
@ -13,6 +14,7 @@ use Filament\Forms;
 | 
			
		||||
use Filament\Forms\Components\Actions as FormActions;
 | 
			
		||||
use Filament\Forms\Components\Fieldset;
 | 
			
		||||
use Filament\Forms\Components\Grid;
 | 
			
		||||
use Filament\Forms\Components\Hidden;
 | 
			
		||||
use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\Tabs;
 | 
			
		||||
use Filament\Forms\Components\Tabs\Tab;
 | 
			
		||||
@ -26,7 +28,7 @@ use Filament\Forms\Set;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Filament\Resources\Pages\EditRecord;
 | 
			
		||||
use Filament\Support\Enums\Alignment;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use Illuminate\Http\Client\ConnectionException;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
 | 
			
		||||
 | 
			
		||||
@ -34,12 +36,13 @@ class EditNode extends EditRecord
 | 
			
		||||
{
 | 
			
		||||
    protected static string $resource = NodeResource::class;
 | 
			
		||||
 | 
			
		||||
    private bool $errored = false;
 | 
			
		||||
    private DaemonConfigurationRepository $daemonConfigurationRepository;
 | 
			
		||||
 | 
			
		||||
    private NodeUpdateService $nodeUpdateService;
 | 
			
		||||
 | 
			
		||||
    public function boot(NodeUpdateService $nodeUpdateService): void
 | 
			
		||||
    public function boot(DaemonConfigurationRepository $daemonConfigurationRepository, NodeUpdateService $nodeUpdateService): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->daemonConfigurationRepository = $daemonConfigurationRepository;
 | 
			
		||||
        $this->nodeUpdateService = $nodeUpdateService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -108,7 +111,8 @@ class EditNode extends EditRecord
 | 
			
		||||
                                ->required()
 | 
			
		||||
                                ->autofocus()
 | 
			
		||||
                                ->live(debounce: 1500)
 | 
			
		||||
                                ->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
 | 
			
		||||
                                ->rules(Node::getRulesForField('fqdn'))
 | 
			
		||||
                                ->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
 | 
			
		||||
                                ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
 | 
			
		||||
                                ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
 | 
			
		||||
                                ->helperText(function ($state) {
 | 
			
		||||
@ -196,7 +200,9 @@ class EditNode extends EditRecord
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->required()
 | 
			
		||||
                                ->maxLength(100),
 | 
			
		||||
                            ToggleButtons::make('scheme')
 | 
			
		||||
                            Hidden::make('scheme'),
 | 
			
		||||
                            Hidden::make('behind_proxy'),
 | 
			
		||||
                            ToggleButtons::make('connection')
 | 
			
		||||
                                ->label(trans('admin/node.ssl'))
 | 
			
		||||
                                ->columnSpan(1)
 | 
			
		||||
                                ->inline()
 | 
			
		||||
@ -211,20 +217,30 @@ class EditNode extends EditRecord
 | 
			
		||||
 | 
			
		||||
                                    return '';
 | 
			
		||||
                                })
 | 
			
		||||
                                ->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
 | 
			
		||||
                                ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
 | 
			
		||||
                                ->options([
 | 
			
		||||
                                    'http' => 'HTTP',
 | 
			
		||||
                                    'https' => 'HTTPS (SSL)',
 | 
			
		||||
                                    'https_proxy' => 'HTTPS with (reverse) proxy',
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->colors([
 | 
			
		||||
                                    'http' => 'warning',
 | 
			
		||||
                                    'https' => 'success',
 | 
			
		||||
                                    'https_proxy' => 'success',
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->icons([
 | 
			
		||||
                                    'http' => 'tabler-lock-open-off',
 | 
			
		||||
                                    'https' => 'tabler-lock',
 | 
			
		||||
                                    'https_proxy' => 'tabler-shield-lock',
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
 | 
			
		||||
                                ->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https'))
 | 
			
		||||
                                ->live()
 | 
			
		||||
                                ->dehydrated(false)
 | 
			
		||||
                                ->afterStateUpdated(function ($state, Set $set) {
 | 
			
		||||
                                    $set('scheme', $state === 'http' ? 'http' : 'https');
 | 
			
		||||
                                    $set('behind_proxy', $state === 'https_proxy');
 | 
			
		||||
                                }),
 | 
			
		||||
                        ]),
 | 
			
		||||
                    Tab::make('adv')
 | 
			
		||||
                        ->label(trans('admin/node.tabs.advanced_settings'))
 | 
			
		||||
                        ->columns([
 | 
			
		||||
@ -596,39 +612,6 @@ class EditNode extends EditRecord
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function handleRecordUpdate(Model $record, array $data): Model
 | 
			
		||||
    {
 | 
			
		||||
        if (!$record instanceof Node) {
 | 
			
		||||
            return $record;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $record = $this->nodeUpdateService->handle($record, $data);
 | 
			
		||||
        } catch (Exception $exception) {
 | 
			
		||||
            $this->errored = true;
 | 
			
		||||
 | 
			
		||||
            Notification::make()
 | 
			
		||||
                ->title(trans('admin/node.error_connecting', ['node' => $record->name]))
 | 
			
		||||
                ->body(trans('admin/node.error_connecting_description'))
 | 
			
		||||
                ->color('warning')
 | 
			
		||||
                ->icon('tabler-database')
 | 
			
		||||
                ->warning()
 | 
			
		||||
                ->send();
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return parent::handleRecordUpdate($record, $data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getSavedNotification(): ?Notification
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->errored) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return parent::getSavedNotification();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getFormActions(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
@ -647,6 +630,31 @@ class EditNode extends EditRecord
 | 
			
		||||
    protected function afterSave(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->fillForm();
 | 
			
		||||
 | 
			
		||||
        /** @var Node $node */
 | 
			
		||||
        $node = $this->record;
 | 
			
		||||
 | 
			
		||||
        $changed = collect($node->getChanges())->except(['updated_at', 'name', 'tags', 'public', 'maintenance_mode', 'memory', 'memory_overallocate', 'disk', 'disk_overallocate', 'cpu', 'cpu_overallocate'])->all();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if ($changed) {
 | 
			
		||||
                $this->daemonConfigurationRepository->setNode($node)->update($node);
 | 
			
		||||
            }
 | 
			
		||||
            parent::getSavedNotification()?->send();
 | 
			
		||||
        } catch (ConnectionException) {
 | 
			
		||||
            Notification::make()
 | 
			
		||||
                ->title(trans('admin/node.error_connecting', ['node' => $node->name]))
 | 
			
		||||
                ->body(trans('admin/node.error_connecting_description'))
 | 
			
		||||
                ->color('warning')
 | 
			
		||||
                ->icon('tabler-database')
 | 
			
		||||
                ->warning()
 | 
			
		||||
                ->send();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getSavedNotification(): ?Notification
 | 
			
		||||
    {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getColumnSpan(): ?int
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,7 @@ use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Get;
 | 
			
		||||
use Filament\Forms\Set;
 | 
			
		||||
use Filament\Resources\RelationManagers\RelationManager;
 | 
			
		||||
use Filament\Tables;
 | 
			
		||||
use Filament\Tables\Actions\BulkActionGroup;
 | 
			
		||||
use Filament\Tables\Actions\Action;
 | 
			
		||||
use Filament\Tables\Actions\DeleteBulkAction;
 | 
			
		||||
use Filament\Tables\Columns\SelectColumn;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
@ -32,18 +31,12 @@ class AllocationsRelationManager extends RelationManager
 | 
			
		||||
    public function setTitle(): string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/server.allocations');
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function table(Table $table): Table
 | 
			
		||||
    {
 | 
			
		||||
        return $table
 | 
			
		||||
            ->recordTitleAttribute('ip')
 | 
			
		||||
 | 
			
		||||
            // Non Primary Allocations
 | 
			
		||||
            // ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
 | 
			
		||||
 | 
			
		||||
            // All assigned allocations
 | 
			
		||||
            ->recordTitleAttribute('address')
 | 
			
		||||
            ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
 | 
			
		||||
            ->paginationPageOptions(['10', '20', '50', '100', '200', '500'])
 | 
			
		||||
            ->searchable()
 | 
			
		||||
@ -72,14 +65,14 @@ class AllocationsRelationManager extends RelationManager
 | 
			
		||||
                    ->label(trans('admin/node.table.ip')),
 | 
			
		||||
            ])
 | 
			
		||||
            ->headerActions([
 | 
			
		||||
                Tables\Actions\Action::make('create new allocation')
 | 
			
		||||
                Action::make('create new allocation')
 | 
			
		||||
                    ->label(trans('admin/node.create_allocation'))
 | 
			
		||||
                    ->form(fn () => [
 | 
			
		||||
                        Select::make('allocation_ip')
 | 
			
		||||
                            ->options(collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
 | 
			
		||||
                            ->label(trans('admin/node.ip_address'))
 | 
			
		||||
                            ->inlineLabel()
 | 
			
		||||
                            ->ipv4()
 | 
			
		||||
                            ->ip()
 | 
			
		||||
                            ->helperText(trans('admin/node.ip_help'))
 | 
			
		||||
                            ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
 | 
			
		||||
                            ->live()
 | 
			
		||||
@ -96,19 +89,15 @@ class AllocationsRelationManager extends RelationManager
 | 
			
		||||
                            ->inlineLabel()
 | 
			
		||||
                            ->live()
 | 
			
		||||
                            ->disabled(fn (Get $get) => empty($get('allocation_ip')))
 | 
			
		||||
                            ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
 | 
			
		||||
                                CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip')))
 | 
			
		||||
                            )
 | 
			
		||||
                            ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip'))))
 | 
			
		||||
                            ->splitKeys(['Tab', ' ', ','])
 | 
			
		||||
                            ->required(),
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
 | 
			
		||||
            ])
 | 
			
		||||
            ->bulkActions([
 | 
			
		||||
                BulkActionGroup::make([
 | 
			
		||||
                    DeleteBulkAction::make()
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can('update node')),
 | 
			
		||||
                ]),
 | 
			
		||||
            ->groupedBulkActions([
 | 
			
		||||
                DeleteBulkAction::make()
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can('update node')),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ class NodeCpuChart extends ChartWidget
 | 
			
		||||
 | 
			
		||||
        $this->cpuHistory = session()->get('cpuHistory', []);
 | 
			
		||||
        $this->cpuHistory[] = [
 | 
			
		||||
            'cpu' => Number::format($data['cpu_percent'] * $threads, maxPrecision: 2),
 | 
			
		||||
            'cpu' => round($data['cpu_percent'] * $threads, 2),
 | 
			
		||||
            'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,9 +27,9 @@ class NodeMemoryChart extends ChartWidget
 | 
			
		||||
 | 
			
		||||
        $this->memoryHistory = session()->get('memoryHistory', []);
 | 
			
		||||
        $this->memoryHistory[] = [
 | 
			
		||||
            'memory' => Number::format(config('panel.use_binary_prefix')
 | 
			
		||||
            'memory' => round(config('panel.use_binary_prefix')
 | 
			
		||||
                ? $value / 1024 / 1024 / 1024
 | 
			
		||||
                : $value / 1000 / 1000 / 1000, maxPrecision: 2),
 | 
			
		||||
                : $value / 1000 / 1000 / 1000, 2),
 | 
			
		||||
            'timestamp' => now(auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets;
 | 
			
		||||
 | 
			
		||||
use App\Models\Node;
 | 
			
		||||
use Filament\Widgets\ChartWidget;
 | 
			
		||||
use Illuminate\Support\Number;
 | 
			
		||||
 | 
			
		||||
class NodeStorageChart extends ChartWidget
 | 
			
		||||
{
 | 
			
		||||
@ -46,8 +45,8 @@ class NodeStorageChart extends ChartWidget
 | 
			
		||||
 | 
			
		||||
        $unused = $total - $used;
 | 
			
		||||
 | 
			
		||||
        $used = Number::format($used, maxPrecision: 2);
 | 
			
		||||
        $unused = Number::format($unused, maxPrecision: 2);
 | 
			
		||||
        $used = round($used, 2);
 | 
			
		||||
        $unused = round($unused, 2);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'datasets' => [
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,6 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Resources;
 | 
			
		||||
 | 
			
		||||
use App\Enums\RolePermissionModels;
 | 
			
		||||
use App\Enums\RolePermissionPrefixes;
 | 
			
		||||
use App\Filament\Admin\Resources\RoleResource\Pages;
 | 
			
		||||
use App\Models\Role;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action;
 | 
			
		||||
@ -12,6 +10,7 @@ use Filament\Forms\Components\Component;
 | 
			
		||||
use Filament\Forms\Components\Fieldset;
 | 
			
		||||
use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\Section;
 | 
			
		||||
use Filament\Forms\Components\Select;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Form;
 | 
			
		||||
use Filament\Forms\Get;
 | 
			
		||||
@ -50,7 +49,7 @@ class RoleResource extends Resource
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationGroup(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.user');
 | 
			
		||||
        return config('panel.filament.top-navigation', false) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationBadge(): ?string
 | 
			
		||||
@ -71,6 +70,11 @@ class RoleResource extends Resource
 | 
			
		||||
                    ->badge()
 | 
			
		||||
                    ->counts('permissions')
 | 
			
		||||
                    ->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? trans('admin/role.all') : $state),
 | 
			
		||||
                TextColumn::make('nodes.name')
 | 
			
		||||
                    ->icon('tabler-server-2')
 | 
			
		||||
                    ->label(trans('admin/role.nodes'))
 | 
			
		||||
                    ->badge()
 | 
			
		||||
                    ->placeholder(trans('admin/role.all')),
 | 
			
		||||
                TextColumn::make('users_count')
 | 
			
		||||
                    ->label(trans('admin/role.users'))
 | 
			
		||||
                    ->counts('users')
 | 
			
		||||
@ -95,32 +99,16 @@ class RoleResource extends Resource
 | 
			
		||||
 | 
			
		||||
    public static function form(Form $form): Form
 | 
			
		||||
    {
 | 
			
		||||
        $permissions = [];
 | 
			
		||||
        $permissionSections = [];
 | 
			
		||||
 | 
			
		||||
        foreach (RolePermissionModels::cases() as $model) {
 | 
			
		||||
        foreach (Role::getPermissionList() as $model => $permissions) {
 | 
			
		||||
            $options = [];
 | 
			
		||||
 | 
			
		||||
            foreach (RolePermissionPrefixes::cases() as $prefix) {
 | 
			
		||||
                $options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix->value);
 | 
			
		||||
            foreach ($permissions as $permission) {
 | 
			
		||||
                $options[$permission . ' ' . strtolower($model)] = Str::headline($permission);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
            $permissionSections[] = self::makeSection($model, $options);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $form
 | 
			
		||||
@ -137,12 +125,20 @@ class RoleResource extends Resource
 | 
			
		||||
                    ->hidden(),
 | 
			
		||||
                Fieldset::make(trans('admin/role.permissions'))
 | 
			
		||||
                    ->columns(3)
 | 
			
		||||
                    ->schema($permissions)
 | 
			
		||||
                    ->schema($permissionSections)
 | 
			
		||||
                    ->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
 | 
			
		||||
                Placeholder::make('permissions')
 | 
			
		||||
                    ->label(trans('admin/role.permissions'))
 | 
			
		||||
                    ->content(trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]))
 | 
			
		||||
                    ->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
 | 
			
		||||
                Select::make('nodes')
 | 
			
		||||
                    ->label(trans('admin/role.nodes'))
 | 
			
		||||
                    ->multiple()
 | 
			
		||||
                    ->relationship('nodes', 'name')
 | 
			
		||||
                    ->searchable(['name', 'fqdn'])
 | 
			
		||||
                    ->preload()
 | 
			
		||||
                    ->hint(trans('admin/role.nodes_hint'))
 | 
			
		||||
                    ->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,12 @@
 | 
			
		||||
namespace App\Filament\Admin\Resources;
 | 
			
		||||
 | 
			
		||||
use App\Filament\Admin\Resources\ServerResource\Pages;
 | 
			
		||||
use App\Models\Mount;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Filament\Forms\Components\CheckboxList;
 | 
			
		||||
use Filament\Forms\Get;
 | 
			
		||||
use Filament\Resources\Resource;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
 | 
			
		||||
class ServerResource extends Resource
 | 
			
		||||
{
 | 
			
		||||
@ -31,12 +35,35 @@ class ServerResource extends Resource
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationGroup(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.server');
 | 
			
		||||
        return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationBadge(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return static::getModel()::count() ?: null;
 | 
			
		||||
        return (string) static::getEloquentQuery()->count() ?: null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getMountCheckboxList(Get $get): CheckboxList
 | 
			
		||||
    {
 | 
			
		||||
        $allowedMounts = Mount::all();
 | 
			
		||||
        $node = $get('node_id');
 | 
			
		||||
        $egg = $get('egg_id');
 | 
			
		||||
 | 
			
		||||
        if ($node && $egg) {
 | 
			
		||||
            $allowedMounts = $allowedMounts->filter(fn (Mount $mount) => ($mount->nodes->isEmpty() || $mount->nodes->contains($node)) &&
 | 
			
		||||
                ($mount->eggs->isEmpty() || $mount->eggs->contains($egg))
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return CheckboxList::make('mounts')
 | 
			
		||||
            ->label('')
 | 
			
		||||
            ->relationship('mounts')
 | 
			
		||||
            ->live()
 | 
			
		||||
            ->options(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
 | 
			
		||||
            ->descriptions(fn () => $allowedMounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
 | 
			
		||||
            ->helperText(fn () => $allowedMounts->isEmpty() ? trans('admin/server.no_mounts') : null)
 | 
			
		||||
            ->bulkToggleable()
 | 
			
		||||
            ->columnSpanFull();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getPages(): array
 | 
			
		||||
@ -47,4 +74,13 @@ class ServerResource extends Resource
 | 
			
		||||
            'edit' => Pages\EditServer::route('/{record}/edit'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getEloquentQuery(): Builder
 | 
			
		||||
    {
 | 
			
		||||
        $query = parent::getEloquentQuery();
 | 
			
		||||
 | 
			
		||||
        return $query->whereHas('node', function (Builder $query) {
 | 
			
		||||
            $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'));
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@ use Closure;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Filament\Forms;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action;
 | 
			
		||||
use Filament\Forms\Components\CheckboxList;
 | 
			
		||||
use Filament\Forms\Components\Component;
 | 
			
		||||
use Filament\Forms\Components\Fieldset;
 | 
			
		||||
use Filament\Forms\Components\Grid;
 | 
			
		||||
@ -109,14 +108,20 @@ class CreateServer extends CreateRecord
 | 
			
		||||
                                ->disabledOn('edit')
 | 
			
		||||
                                ->prefixIcon('tabler-server-2')
 | 
			
		||||
                                ->selectablePlaceholder(false)
 | 
			
		||||
                                ->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
 | 
			
		||||
                                ->default(function () {
 | 
			
		||||
                                    /** @var ?Node $latestNode */
 | 
			
		||||
                                    $latestNode = auth()->user()->accessibleNodes()->latest()->first();
 | 
			
		||||
                                    $this->node = $latestNode;
 | 
			
		||||
 | 
			
		||||
                                    return $this->node?->id;
 | 
			
		||||
                                })
 | 
			
		||||
                                ->columnSpan([
 | 
			
		||||
                                    'default' => 1,
 | 
			
		||||
                                    'sm' => 2,
 | 
			
		||||
                                    'md' => 2,
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->live()
 | 
			
		||||
                                ->relationship('node', 'name')
 | 
			
		||||
                                ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
 | 
			
		||||
                                ->searchable()
 | 
			
		||||
                                ->preload()
 | 
			
		||||
                                ->afterStateUpdated(function (Set $set, $state) {
 | 
			
		||||
@ -183,10 +188,7 @@ class CreateServer extends CreateRecord
 | 
			
		||||
                                    $set('allocation_additional', null);
 | 
			
		||||
                                    $set('allocation_additional.needstobeastringhere.extra_allocations', null);
 | 
			
		||||
                                })
 | 
			
		||||
                                ->getOptionLabelFromRecordUsing(
 | 
			
		||||
                                    fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
 | 
			
		||||
                                        ($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
 | 
			
		||||
                                )
 | 
			
		||||
                                ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
 | 
			
		||||
                                ->placeholder(function (Get $get) {
 | 
			
		||||
                                    $node = Node::find($get('node_id'));
 | 
			
		||||
 | 
			
		||||
@ -212,7 +214,7 @@ class CreateServer extends CreateRecord
 | 
			
		||||
                                            ->label(trans('admin/server.ip_address'))->inlineLabel()
 | 
			
		||||
                                            ->helperText(trans('admin/server.ip_address_helper'))
 | 
			
		||||
                                            ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
 | 
			
		||||
                                            ->ipv4()
 | 
			
		||||
                                            ->ip()
 | 
			
		||||
                                            ->live()
 | 
			
		||||
                                            ->required(),
 | 
			
		||||
                                        TextInput::make('allocation_alias')
 | 
			
		||||
@ -263,10 +265,7 @@ class CreateServer extends CreateRecord
 | 
			
		||||
                                        ->columnSpan(2)
 | 
			
		||||
                                        ->disabled(fn (Get $get) => $get('../../node_id') === null)
 | 
			
		||||
                                        ->searchable(['ip', 'port', 'ip_alias'])
 | 
			
		||||
                                        ->getOptionLabelFromRecordUsing(
 | 
			
		||||
                                            fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
 | 
			
		||||
                                                ($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
 | 
			
		||||
                                        )
 | 
			
		||||
                                        ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
 | 
			
		||||
                                        ->placeholder(trans('admin/server.select_additional'))
 | 
			
		||||
                                        ->disableOptionsWhenSelectedInSiblingRepeaterItems()
 | 
			
		||||
                                        ->relationship(
 | 
			
		||||
@ -426,7 +425,7 @@ class CreateServer extends CreateRecord
 | 
			
		||||
 | 
			
		||||
                                    Repeater::make('server_variables')
 | 
			
		||||
                                        ->label('')
 | 
			
		||||
                                        ->relationship('serverVariables')
 | 
			
		||||
                                        ->relationship('serverVariables', fn (Builder $query) => $query->orderByPowerJoins('variable.sort'))
 | 
			
		||||
                                        ->saveRelationshipsBeforeChildrenUsing(null)
 | 
			
		||||
                                        ->saveRelationshipsUsing(null)
 | 
			
		||||
                                        ->grid(2)
 | 
			
		||||
@ -744,7 +743,7 @@ class CreateServer extends CreateRecord
 | 
			
		||||
                                    'lg' => 4,
 | 
			
		||||
                                ])
 | 
			
		||||
                                ->columnSpan(6)
 | 
			
		||||
                                ->schema([
 | 
			
		||||
                                ->schema(fn (Get $get) => [
 | 
			
		||||
                                    Select::make('select_image')
 | 
			
		||||
                                        ->label(trans('admin/server.image_name'))
 | 
			
		||||
                                        ->live()
 | 
			
		||||
@ -792,19 +791,13 @@ class CreateServer extends CreateRecord
 | 
			
		||||
                                        ]),
 | 
			
		||||
 | 
			
		||||
                                    KeyValue::make('docker_labels')
 | 
			
		||||
                                        ->live()
 | 
			
		||||
                                        ->label('Container Labels')
 | 
			
		||||
                                        ->keyLabel(trans('admin/server.title'))
 | 
			
		||||
                                        ->valueLabel(trans('admin/server.description'))
 | 
			
		||||
                                        ->columnSpanFull(),
 | 
			
		||||
 | 
			
		||||
                                    CheckboxList::make('mounts')
 | 
			
		||||
                                        ->label('Mounts')
 | 
			
		||||
                                        ->live()
 | 
			
		||||
                                        ->relationship('mounts')
 | 
			
		||||
                                        ->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
 | 
			
		||||
                                        ->descriptions(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]) ?? [])
 | 
			
		||||
                                        ->helperText(fn () => $this->node?->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
 | 
			
		||||
                                        ->columnSpanFull(),
 | 
			
		||||
                                    ServerResource::getMountCheckboxList($get),
 | 
			
		||||
                                ]),
 | 
			
		||||
                        ]),
 | 
			
		||||
                ])
 | 
			
		||||
 | 
			
		||||
@ -2,16 +2,18 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Resources\ServerResource\Pages;
 | 
			
		||||
 | 
			
		||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
 | 
			
		||||
use App\Enums\SuspendAction;
 | 
			
		||||
use App\Filament\Admin\Resources\ServerResource;
 | 
			
		||||
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
 | 
			
		||||
use App\Filament\Components\Forms\Actions\PreviewStartupAction;
 | 
			
		||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
 | 
			
		||||
use App\Filament\Server\Pages\Console;
 | 
			
		||||
use App\Models\Allocation;
 | 
			
		||||
use App\Models\Database;
 | 
			
		||||
use App\Models\DatabaseHost;
 | 
			
		||||
use App\Models\Egg;
 | 
			
		||||
use App\Models\Mount;
 | 
			
		||||
use App\Models\Node;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Models\ServerVariable;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
@ -28,8 +30,9 @@ use Closure;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Filament\Actions;
 | 
			
		||||
use Filament\Forms;
 | 
			
		||||
use Filament\Forms\Components\Actions as FormActions;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action;
 | 
			
		||||
use Filament\Forms\Components\CheckboxList;
 | 
			
		||||
use Filament\Forms\Components\Component;
 | 
			
		||||
use Filament\Forms\Components\Fieldset;
 | 
			
		||||
use Filament\Forms\Components\Grid;
 | 
			
		||||
use Filament\Forms\Components\Hidden;
 | 
			
		||||
@ -48,10 +51,12 @@ use Filament\Forms\Get;
 | 
			
		||||
use Filament\Forms\Set;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Filament\Resources\Pages\EditRecord;
 | 
			
		||||
use Filament\Support\Enums\Alignment;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use Illuminate\Http\Client\ConnectionException;
 | 
			
		||||
use Illuminate\Support\Arr;
 | 
			
		||||
use Illuminate\Support\Facades\Validator;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
use LogicException;
 | 
			
		||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
 | 
			
		||||
 | 
			
		||||
@ -59,8 +64,6 @@ class EditServer extends EditRecord
 | 
			
		||||
{
 | 
			
		||||
    protected static string $resource = ServerResource::class;
 | 
			
		||||
 | 
			
		||||
    private bool $errored = false;
 | 
			
		||||
 | 
			
		||||
    private DaemonServerRepository $daemonServerRepository;
 | 
			
		||||
 | 
			
		||||
    public function boot(DaemonServerRepository $daemonServerRepository): void
 | 
			
		||||
@ -133,7 +136,39 @@ class EditServer extends EditRecord
 | 
			
		||||
                                        'sm' => 1,
 | 
			
		||||
                                        'md' => 1,
 | 
			
		||||
                                        'lg' => 1,
 | 
			
		||||
                                    ]),
 | 
			
		||||
                                    ])
 | 
			
		||||
                                    ->hintAction(
 | 
			
		||||
                                        Action::make('view_install_log')
 | 
			
		||||
                                            ->label(trans('admin/server.view_install_log'))
 | 
			
		||||
                                            //->visible(fn (Server $server) => $server->isFailedInstall())
 | 
			
		||||
                                            ->modalHeading('')
 | 
			
		||||
                                            ->modalSubmitAction(false)
 | 
			
		||||
                                            ->modalFooterActionsAlignment(Alignment::Right)
 | 
			
		||||
                                            ->modalCancelActionLabel(trans('filament::components/modal.actions.close.label'))
 | 
			
		||||
                                            ->form([
 | 
			
		||||
                                                MonacoEditor::make('logs')
 | 
			
		||||
                                                    ->hiddenLabel()
 | 
			
		||||
                                                    ->placeholderText(trans('admin/server.no_log'))
 | 
			
		||||
                                                    ->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) {
 | 
			
		||||
                                                        try {
 | 
			
		||||
                                                            return $serverRepository->setServer($server)->getInstallLogs();
 | 
			
		||||
                                                        } catch (ConnectionException) {
 | 
			
		||||
                                                            Notification::make()
 | 
			
		||||
                                                                ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
 | 
			
		||||
                                                                ->body(trans('admin/server.notifications.log_failed'))
 | 
			
		||||
                                                                ->color('warning')
 | 
			
		||||
                                                                ->warning()
 | 
			
		||||
                                                                ->send();
 | 
			
		||||
                                                        } catch (Exception) {
 | 
			
		||||
                                                            return '';
 | 
			
		||||
                                                        }
 | 
			
		||||
 | 
			
		||||
                                                        return '';
 | 
			
		||||
                                                    })
 | 
			
		||||
                                                    ->language('shell')
 | 
			
		||||
                                                    ->view('filament.plugins.monaco-editor-logs'),
 | 
			
		||||
                                            ])
 | 
			
		||||
                                    ),
 | 
			
		||||
 | 
			
		||||
                                Textarea::make('description')
 | 
			
		||||
                                    ->label(trans('admin/server.description'))
 | 
			
		||||
@ -173,7 +208,7 @@ class EditServer extends EditRecord
 | 
			
		||||
                                    ->maxLength(255),
 | 
			
		||||
                                Select::make('node_id')
 | 
			
		||||
                                    ->label(trans('admin/server.node'))
 | 
			
		||||
                                    ->relationship('node', 'name')
 | 
			
		||||
                                    ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
 | 
			
		||||
                                    ->columnSpan([
 | 
			
		||||
                                        'default' => 2,
 | 
			
		||||
                                        'sm' => 1,
 | 
			
		||||
@ -482,6 +517,7 @@ class EditServer extends EditRecord
 | 
			
		||||
                                            ]),
 | 
			
		||||
 | 
			
		||||
                                        KeyValue::make('docker_labels')
 | 
			
		||||
                                            ->live()
 | 
			
		||||
                                            ->label(trans('admin/server.container_labels'))
 | 
			
		||||
                                            ->keyLabel(trans('admin/server.title'))
 | 
			
		||||
                                            ->valueLabel(trans('admin/server.description'))
 | 
			
		||||
@ -591,7 +627,7 @@ class EditServer extends EditRecord
 | 
			
		||||
                                            ]);
 | 
			
		||||
                                        }
 | 
			
		||||
 | 
			
		||||
                                        return $query;
 | 
			
		||||
                                        return $query->orderByPowerJoins('variable.sort');
 | 
			
		||||
                                    })
 | 
			
		||||
                                    ->grid()
 | 
			
		||||
                                    ->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
 | 
			
		||||
@ -646,14 +682,8 @@ class EditServer extends EditRecord
 | 
			
		||||
                            ]),
 | 
			
		||||
                        Tab::make(trans('admin/server.mounts'))
 | 
			
		||||
                            ->icon('tabler-layers-linked')
 | 
			
		||||
                            ->schema([
 | 
			
		||||
                                CheckboxList::make('mounts')
 | 
			
		||||
                                    ->label('')
 | 
			
		||||
                                    ->relationship('mounts')
 | 
			
		||||
                                    ->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
 | 
			
		||||
                                    ->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
 | 
			
		||||
                                    ->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : trans('admin/server.no_mounts'))
 | 
			
		||||
                                    ->columnSpanFull(),
 | 
			
		||||
                            ->schema(fn (Get $get) => [
 | 
			
		||||
                                ServerResource::getMountCheckboxList($get),
 | 
			
		||||
                            ]),
 | 
			
		||||
                        Tab::make(trans('admin/server.databases'))
 | 
			
		||||
                            ->hidden(fn () => !auth()->user()->can('viewList database'))
 | 
			
		||||
@ -686,8 +716,8 @@ class EditServer extends EditRecord
 | 
			
		||||
                                                    ->requiresConfirmation()
 | 
			
		||||
                                                    ->modalIcon('tabler-database-x')
 | 
			
		||||
                                                    ->modalHeading(trans('admin/server.delete_db_heading'))
 | 
			
		||||
                                                    ->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
 | 
			
		||||
                                                    ->modalDescription(fn (Get $get) => trans('admin/server.delete_db') . $get('database') . '?')
 | 
			
		||||
                                                    ->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
 | 
			
		||||
                                                    ->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')]))
 | 
			
		||||
                                                    ->action(function (DatabaseManagementService $databaseManagementService, $record) {
 | 
			
		||||
                                                        $databaseManagementService->delete($record);
 | 
			
		||||
                                                        $this->fillForm();
 | 
			
		||||
@ -731,7 +761,7 @@ class EditServer extends EditRecord
 | 
			
		||||
                                    ->deletable(false)
 | 
			
		||||
                                    ->addable(false)
 | 
			
		||||
                                    ->columnSpan(4),
 | 
			
		||||
                                Forms\Components\Actions::make([
 | 
			
		||||
                                FormActions::make([
 | 
			
		||||
                                    Action::make('createDatabase')
 | 
			
		||||
                                        ->authorize(fn () => auth()->user()->can('create database'))
 | 
			
		||||
                                        ->disabled(fn () => DatabaseHost::query()->count() < 1)
 | 
			
		||||
@ -799,14 +829,50 @@ class EditServer extends EditRecord
 | 
			
		||||
                                        Grid::make()
 | 
			
		||||
                                            ->columnSpan(3)
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Forms\Components\Actions::make([
 | 
			
		||||
                                                FormActions::make([
 | 
			
		||||
                                                    Action::make('toggleInstall')
 | 
			
		||||
                                                        ->label(trans('admin/server.toggle_install'))
 | 
			
		||||
                                                        ->disabled(fn (Server $server) => $server->isSuspended())
 | 
			
		||||
                                                        ->action(function (ToggleInstallService $service, Server $server) {
 | 
			
		||||
                                                            $service->handle($server);
 | 
			
		||||
                                                        ->modal(fn (Server $server) => $server->isFailedInstall())
 | 
			
		||||
                                                        ->modalHeading(trans('admin/server.toggle_install_failed_header'))
 | 
			
		||||
                                                        ->modalDescription(trans('admin/server.toggle_install_failed_desc'))
 | 
			
		||||
                                                        ->modalSubmitActionLabel(trans('admin/server.reinstall'))
 | 
			
		||||
                                                        ->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) {
 | 
			
		||||
                                                            if ($server->isFailedInstall()) {
 | 
			
		||||
                                                                try {
 | 
			
		||||
                                                                    $reinstallService->handle($server);
 | 
			
		||||
 | 
			
		||||
                                                            $this->refreshFormData(['status', 'docker']);
 | 
			
		||||
                                                                    Notification::make()
 | 
			
		||||
                                                                        ->title(trans('admin/server.notifications.reinstall_started'))
 | 
			
		||||
                                                                        ->success()
 | 
			
		||||
                                                                        ->send();
 | 
			
		||||
 | 
			
		||||
                                                                    $this->refreshFormData(['status', 'docker']);
 | 
			
		||||
                                                                } catch (Exception) {
 | 
			
		||||
                                                                    Notification::make()
 | 
			
		||||
                                                                        ->title(trans('admin/server.notifications.reinstall_failed'))
 | 
			
		||||
                                                                        ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
 | 
			
		||||
                                                                        ->danger()
 | 
			
		||||
                                                                        ->send();
 | 
			
		||||
                                                                }
 | 
			
		||||
                                                            } else {
 | 
			
		||||
                                                                try {
 | 
			
		||||
                                                                    $toggleService->handle($server);
 | 
			
		||||
 | 
			
		||||
                                                                    Notification::make()
 | 
			
		||||
                                                                        ->title(trans('admin/server.notifications.install_toggled'))
 | 
			
		||||
                                                                        ->success()
 | 
			
		||||
                                                                        ->send();
 | 
			
		||||
 | 
			
		||||
                                                                    $this->refreshFormData(['status', 'docker']);
 | 
			
		||||
                                                                } catch (Exception $exception) {
 | 
			
		||||
                                                                    Notification::make()
 | 
			
		||||
                                                                        ->title(trans('admin/server.notifications.install_toggle_failed'))
 | 
			
		||||
                                                                        ->body($exception->getMessage())
 | 
			
		||||
                                                                        ->danger()
 | 
			
		||||
                                                                        ->send();
 | 
			
		||||
                                                                }
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                        }),
 | 
			
		||||
                                                ])->fullWidth(),
 | 
			
		||||
                                                ToggleButtons::make('')
 | 
			
		||||
@ -815,7 +881,7 @@ class EditServer extends EditRecord
 | 
			
		||||
                                        Grid::make()
 | 
			
		||||
                                            ->columnSpan(3)
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Forms\Components\Actions::make([
 | 
			
		||||
                                                FormActions::make([
 | 
			
		||||
                                                    Action::make('toggleSuspend')
 | 
			
		||||
                                                        ->label(trans('admin/server.suspend'))
 | 
			
		||||
                                                        ->color('warning')
 | 
			
		||||
@ -823,12 +889,20 @@ class EditServer extends EditRecord
 | 
			
		||||
                                                        ->action(function (SuspensionService $suspensionService, Server $server) {
 | 
			
		||||
                                                            try {
 | 
			
		||||
                                                                $suspensionService->handle($server, SuspendAction::Suspend);
 | 
			
		||||
                                                            } catch (\Exception $exception) {
 | 
			
		||||
                                                                Notification::make()->warning()->title(trans('admin/server.notifications.server_suspension'))->body($exception->getMessage())->send();
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                            Notification::make()->success()->title(trans('admin/server.notifications.server_suspended'))->send();
 | 
			
		||||
 | 
			
		||||
                                                            $this->refreshFormData(['status', 'docker']);
 | 
			
		||||
                                                                Notification::make()
 | 
			
		||||
                                                                    ->success()
 | 
			
		||||
                                                                    ->title(trans('admin/server.notifications.server_suspended'))
 | 
			
		||||
                                                                    ->send();
 | 
			
		||||
 | 
			
		||||
                                                                $this->refreshFormData(['status', 'docker']);
 | 
			
		||||
                                                            } catch (Exception) {
 | 
			
		||||
                                                                Notification::make()
 | 
			
		||||
                                                                    ->warning()
 | 
			
		||||
                                                                    ->title(trans('admin/server.notifications.server_suspension'))
 | 
			
		||||
                                                                    ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
 | 
			
		||||
                                                                    ->send();
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                        }),
 | 
			
		||||
                                                    Action::make('toggleUnsuspend')
 | 
			
		||||
                                                        ->label(trans('admin/server.unsuspend'))
 | 
			
		||||
@ -837,12 +911,20 @@ class EditServer extends EditRecord
 | 
			
		||||
                                                        ->action(function (SuspensionService $suspensionService, Server $server) {
 | 
			
		||||
                                                            try {
 | 
			
		||||
                                                                $suspensionService->handle($server, SuspendAction::Unsuspend);
 | 
			
		||||
                                                            } catch (\Exception $exception) {
 | 
			
		||||
                                                                Notification::make()->warning()->title(trans('admin/server.notifications.server_suspension'))->body($exception->getMessage())->send();
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                            Notification::make()->success()->title(trans('admin/server.notifications.server_unsuspended'))->send();
 | 
			
		||||
 | 
			
		||||
                                                            $this->refreshFormData(['status', 'docker']);
 | 
			
		||||
                                                                Notification::make()
 | 
			
		||||
                                                                    ->success()
 | 
			
		||||
                                                                    ->title(trans('admin/server.notifications.server_unsuspended'))
 | 
			
		||||
                                                                    ->send();
 | 
			
		||||
 | 
			
		||||
                                                                $this->refreshFormData(['status', 'docker']);
 | 
			
		||||
                                                            } catch (Exception) {
 | 
			
		||||
                                                                Notification::make()
 | 
			
		||||
                                                                    ->warning()
 | 
			
		||||
                                                                    ->title(trans('admin/server.notifications.server_suspension'))
 | 
			
		||||
                                                                    ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
 | 
			
		||||
                                                                    ->send();
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                        }),
 | 
			
		||||
                                                ])->fullWidth(),
 | 
			
		||||
                                                ToggleButtons::make('')
 | 
			
		||||
@ -855,42 +937,36 @@ class EditServer extends EditRecord
 | 
			
		||||
                                        Grid::make()
 | 
			
		||||
                                            ->columnSpan(3)
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Forms\Components\Actions::make([
 | 
			
		||||
                                                FormActions::make([
 | 
			
		||||
                                                    Action::make('transfer')
 | 
			
		||||
                                                        ->label(trans('admin/server.transfer'))
 | 
			
		||||
                                                        // ->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
 | 
			
		||||
                                                        ->disabled() //TODO!
 | 
			
		||||
                                                        ->form([ //TODO!
 | 
			
		||||
                                                            Select::make('newNode')
 | 
			
		||||
                                                                ->label('New Node')
 | 
			
		||||
                                                                ->required()
 | 
			
		||||
                                                                ->options([
 | 
			
		||||
                                                                    true => 'on',
 | 
			
		||||
                                                                    false => 'off',
 | 
			
		||||
                                                                ]),
 | 
			
		||||
                                                            Select::make('newMainAllocation')
 | 
			
		||||
                                                                ->label('New Main Allocation')
 | 
			
		||||
                                                                ->required()
 | 
			
		||||
                                                                ->options([
 | 
			
		||||
                                                                    true => 'on',
 | 
			
		||||
                                                                    false => 'off',
 | 
			
		||||
                                                                ]),
 | 
			
		||||
                                                            Select::make('newAdditionalAllocation')
 | 
			
		||||
                                                                ->label('New Additional Allocations')
 | 
			
		||||
                                                                ->options([
 | 
			
		||||
                                                                    true => 'on',
 | 
			
		||||
                                                                    false => 'off',
 | 
			
		||||
                                                                ]),
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->modalheading(trans('admin/server.transfer')),
 | 
			
		||||
                                                        ->disabled(fn (Server $server) => Node::count() <= 1 || $server->isInConflictState())
 | 
			
		||||
                                                        ->modalheading(trans('admin/server.transfer'))
 | 
			
		||||
                                                        ->form($this->transferServer())
 | 
			
		||||
                                                        ->action(function (TransferServerService $transfer, Server $server, $data) {
 | 
			
		||||
                                                            try {
 | 
			
		||||
                                                                $transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', []));
 | 
			
		||||
 | 
			
		||||
                                                                Notification::make()
 | 
			
		||||
                                                                    ->title('Transfer started')
 | 
			
		||||
                                                                    ->success()
 | 
			
		||||
                                                                    ->send();
 | 
			
		||||
                                                            } catch (Exception $exception) {
 | 
			
		||||
                                                                Notification::make()
 | 
			
		||||
                                                                    ->title('Transfer failed')
 | 
			
		||||
                                                                    ->body($exception->getMessage())
 | 
			
		||||
                                                                    ->danger()
 | 
			
		||||
                                                                    ->send();
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                        }),
 | 
			
		||||
                                                ])->fullWidth(),
 | 
			
		||||
                                                ToggleButtons::make('')
 | 
			
		||||
                                                    ->hint(trans('admin/server.transfer_help')),
 | 
			
		||||
                                                    ->hint(new HtmlString(trans('admin/server.transfer_help'))),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Grid::make()
 | 
			
		||||
                                            ->columnSpan(3)
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Forms\Components\Actions::make([
 | 
			
		||||
                                                FormActions::make([
 | 
			
		||||
                                                    Action::make('reinstall')
 | 
			
		||||
                                                        ->label(trans('admin/server.reinstall'))
 | 
			
		||||
                                                        ->color('danger')
 | 
			
		||||
@ -898,7 +974,24 @@ class EditServer extends EditRecord
 | 
			
		||||
                                                        ->modalHeading(trans('admin/server.reinstall_modal_heading'))
 | 
			
		||||
                                                        ->modalDescription(trans('admin/server.reinstall_modal_description'))
 | 
			
		||||
                                                        ->disabled(fn (Server $server) => $server->isSuspended())
 | 
			
		||||
                                                        ->action(fn (ReinstallServerService $service, Server $server) => $service->handle($server)),
 | 
			
		||||
                                                        ->action(function (ReinstallServerService $service, Server $server) {
 | 
			
		||||
                                                            try {
 | 
			
		||||
                                                                $service->handle($server);
 | 
			
		||||
 | 
			
		||||
                                                                Notification::make()
 | 
			
		||||
                                                                    ->title(trans('admin/server.notifications.reinstall_started'))
 | 
			
		||||
                                                                    ->success()
 | 
			
		||||
                                                                    ->send();
 | 
			
		||||
 | 
			
		||||
                                                                $this->refreshFormData(['status', 'docker']);
 | 
			
		||||
                                                            } catch (Exception) {
 | 
			
		||||
                                                                Notification::make()
 | 
			
		||||
                                                                    ->title(trans('admin/server.notifications.reinstall_failed'))
 | 
			
		||||
                                                                    ->body(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
 | 
			
		||||
                                                                    ->danger()
 | 
			
		||||
                                                                    ->send();
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                        }),
 | 
			
		||||
                                                ])->fullWidth(),
 | 
			
		||||
                                                ToggleButtons::make('')
 | 
			
		||||
                                                    ->hint(trans('admin/server.reinstall_help')),
 | 
			
		||||
@ -909,32 +1002,86 @@ class EditServer extends EditRecord
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function transferServer(Form $form): Form
 | 
			
		||||
    /** @return Component[] */
 | 
			
		||||
    protected function transferServer(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $form
 | 
			
		||||
            ->columns()
 | 
			
		||||
            ->schema([
 | 
			
		||||
                Select::make('toNode')
 | 
			
		||||
                    ->label('New Node'),
 | 
			
		||||
                TextInput::make('newAllocation')
 | 
			
		||||
                    ->label('Allocation'),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            Select::make('node_id')
 | 
			
		||||
                ->label(trans('admin/server.node'))
 | 
			
		||||
                ->prefixIcon('tabler-server-2')
 | 
			
		||||
                ->selectablePlaceholder(false)
 | 
			
		||||
                ->default(fn (Server $server) => Node::whereNot('id', $server->node->id)->first()?->id)
 | 
			
		||||
                ->required()
 | 
			
		||||
                ->live()
 | 
			
		||||
                ->options(fn (Server $server) => Node::whereNot('id', $server->node->id)->pluck('name', 'id')->all()),
 | 
			
		||||
            Select::make('allocation_id')
 | 
			
		||||
                ->label(trans('admin/server.primary_allocation'))
 | 
			
		||||
                ->required()
 | 
			
		||||
                ->prefixIcon('tabler-network')
 | 
			
		||||
                ->disabled(fn (Get $get) => !$get('node_id'))
 | 
			
		||||
                ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address]))
 | 
			
		||||
                ->searchable(['ip', 'port', 'ip_alias'])
 | 
			
		||||
                ->placeholder(trans('admin/server.select_allocation')),
 | 
			
		||||
            Select::make('allocation_additional')
 | 
			
		||||
                ->label(trans('admin/server.additional_allocations'))
 | 
			
		||||
                ->multiple()
 | 
			
		||||
                ->prefixIcon('tabler-network')
 | 
			
		||||
                ->disabled(fn (Get $get) => !$get('node_id'))
 | 
			
		||||
                ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address]))
 | 
			
		||||
                ->searchable(['ip', 'port', 'ip_alias'])
 | 
			
		||||
                ->placeholder(trans('admin/server.select_additional')),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getHeaderActions(): array
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = $this->getRecord();
 | 
			
		||||
 | 
			
		||||
        $canForceDelete = cache()->get("servers.$server->uuid.canForceDelete", false);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            Actions\Action::make('Delete')
 | 
			
		||||
                ->successRedirectUrl(route('filament.admin.resources.servers.index'))
 | 
			
		||||
                ->color('danger')
 | 
			
		||||
                ->label(trans('filament-actions::delete.single.modal.actions.delete.label'))
 | 
			
		||||
                ->label(trans('filament-actions::delete.single.label'))
 | 
			
		||||
                ->modalHeading(trans('filament-actions::delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
 | 
			
		||||
                ->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
 | 
			
		||||
                ->requiresConfirmation()
 | 
			
		||||
                ->action(function (Server $server, ServerDeletionService $service) {
 | 
			
		||||
                    $service->handle($server);
 | 
			
		||||
                    try {
 | 
			
		||||
                        $service->handle($server);
 | 
			
		||||
 | 
			
		||||
                    return redirect(ListServers::getUrl(panel: 'admin'));
 | 
			
		||||
                        return redirect(ListServers::getUrl(panel: 'admin'));
 | 
			
		||||
                    } catch (ConnectionException) {
 | 
			
		||||
                        cache()->put("servers.$server->uuid.canForceDelete", true, now()->addMinutes(5));
 | 
			
		||||
 | 
			
		||||
                        Notification::make()
 | 
			
		||||
                            ->title(trans('admin/server.notifications.error_server_delete'))
 | 
			
		||||
                            ->body(trans('admin/server.notifications.error_server_delete_body'))
 | 
			
		||||
                            ->color('warning')
 | 
			
		||||
                            ->icon('tabler-database')
 | 
			
		||||
                            ->warning()
 | 
			
		||||
                            ->send();
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                ->hidden(fn () => $canForceDelete)
 | 
			
		||||
                ->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
 | 
			
		||||
            Actions\Action::make('ForceDelete')
 | 
			
		||||
                ->color('danger')
 | 
			
		||||
                ->label(trans('filament-actions::force-delete.single.label'))
 | 
			
		||||
                ->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
 | 
			
		||||
                ->modalSubmitActionLabel(trans('filament-actions::force-delete.single.label'))
 | 
			
		||||
                ->requiresConfirmation()
 | 
			
		||||
                ->action(function (Server $server, ServerDeletionService $service) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        $service->withForce()->handle($server);
 | 
			
		||||
 | 
			
		||||
                        return redirect(ListServers::getUrl(panel: 'admin'));
 | 
			
		||||
                    } catch (ConnectionException) {
 | 
			
		||||
                        cache()->forget("servers.$server->uuid.canForceDelete");
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                ->visible(fn () => $canForceDelete)
 | 
			
		||||
                ->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
 | 
			
		||||
            Actions\Action::make('console')
 | 
			
		||||
                ->label(trans('admin/server.console'))
 | 
			
		||||
@ -956,44 +1103,37 @@ class EditServer extends EditRecord
 | 
			
		||||
            $data['description'] = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        unset($data['docker'], $data['status']);
 | 
			
		||||
        unset($data['docker'], $data['status'], $data['allocation_id']);
 | 
			
		||||
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function handleRecordUpdate(Model $record, array $data): Model
 | 
			
		||||
    protected function afterSave(): void
 | 
			
		||||
    {
 | 
			
		||||
        if (!$record instanceof Server) {
 | 
			
		||||
            return $record;
 | 
			
		||||
        }
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = $this->record;
 | 
			
		||||
 | 
			
		||||
        /** @var Server $record */
 | 
			
		||||
        $record = parent::handleRecordUpdate($record, $data);
 | 
			
		||||
        $changed = collect($server->getChanges())->except(['updated_at', 'name', 'owner_id', 'condition', 'description', 'external_id', 'tags', 'cpu_pinning', 'allocation_limit', 'database_limit', 'backup_limit', 'skip_scripts'])->all();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $this->daemonServerRepository->setServer($record)->sync();
 | 
			
		||||
            if ($changed) {
 | 
			
		||||
                $this->daemonServerRepository->setServer($server)->sync();
 | 
			
		||||
            }
 | 
			
		||||
            parent::getSavedNotification()?->send();
 | 
			
		||||
        } catch (ConnectionException) {
 | 
			
		||||
            $this->errored = true;
 | 
			
		||||
 | 
			
		||||
            Notification::make()
 | 
			
		||||
                ->title(trans('admin/server.notifications.error_connecting', ['node' => $record->node->name]))
 | 
			
		||||
                ->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
 | 
			
		||||
                ->body(trans('admin/server.notifications.error_connecting_description'))
 | 
			
		||||
                ->color('warning')
 | 
			
		||||
                ->icon('tabler-database')
 | 
			
		||||
                ->warning()
 | 
			
		||||
                ->send();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $record;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getSavedNotification(): ?Notification
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->errored) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return parent::getSavedNotification();
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRelationManagers(): array
 | 
			
		||||
 | 
			
		||||
@ -12,14 +12,17 @@ use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Get;
 | 
			
		||||
use Filament\Forms\Set;
 | 
			
		||||
use Filament\Resources\RelationManagers\RelationManager;
 | 
			
		||||
use Filament\Tables;
 | 
			
		||||
use Filament\Support\Exceptions\Halt;
 | 
			
		||||
use Filament\Tables\Actions\Action;
 | 
			
		||||
use Filament\Tables\Actions\AssociateAction;
 | 
			
		||||
use Filament\Tables\Actions\CreateAction;
 | 
			
		||||
use Filament\Tables\Actions\DissociateAction;
 | 
			
		||||
use Filament\Tables\Actions\DissociateBulkAction;
 | 
			
		||||
use Filament\Tables\Columns\IconColumn;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Columns\TextInputColumn;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Illuminate\Database\Eloquent\Collection;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @method Server getOwnerRecord()
 | 
			
		||||
@ -32,15 +35,18 @@ class AllocationsRelationManager extends RelationManager
 | 
			
		||||
    {
 | 
			
		||||
        return $table
 | 
			
		||||
            ->selectCurrentPageOnly()
 | 
			
		||||
            ->recordTitleAttribute('ip')
 | 
			
		||||
            ->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
 | 
			
		||||
            ->recordTitleAttribute('address')
 | 
			
		||||
            ->recordTitle(fn (Allocation $allocation) => $allocation->address)
 | 
			
		||||
            ->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
 | 
			
		||||
            ->inverseRelationship('server')
 | 
			
		||||
            ->heading(trans('admin/server.allocations'))
 | 
			
		||||
            ->columns([
 | 
			
		||||
                TextColumn::make('ip')->label(trans('admin/server.ip_address')),
 | 
			
		||||
                TextColumn::make('port')->label(trans('admin/server.port')),
 | 
			
		||||
                TextInputColumn::make('ip_alias')->label(trans('admin/server.alias')),
 | 
			
		||||
                TextColumn::make('ip')
 | 
			
		||||
                    ->label(trans('admin/server.ip_address')),
 | 
			
		||||
                TextColumn::make('port')
 | 
			
		||||
                    ->label(trans('admin/server.port')),
 | 
			
		||||
                TextInputColumn::make('ip_alias')
 | 
			
		||||
                    ->label(trans('admin/server.alias')),
 | 
			
		||||
                IconColumn::make('primary')
 | 
			
		||||
                    ->icon(fn ($state) => match ($state) {
 | 
			
		||||
                        true => 'tabler-star-filled',
 | 
			
		||||
@ -56,8 +62,11 @@ class AllocationsRelationManager extends RelationManager
 | 
			
		||||
            ])
 | 
			
		||||
            ->actions([
 | 
			
		||||
                Action::make('make-primary')
 | 
			
		||||
                    ->label(trans('admin/server.make_primary'))
 | 
			
		||||
                    ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
 | 
			
		||||
                    ->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : trans('admin/server.make_primary')),
 | 
			
		||||
                    ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
 | 
			
		||||
                DissociateAction::make()
 | 
			
		||||
                    ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
 | 
			
		||||
            ])
 | 
			
		||||
            ->headerActions([
 | 
			
		||||
                CreateAction::make()->label(trans('admin/server.create_allocation'))
 | 
			
		||||
@ -67,7 +76,8 @@ class AllocationsRelationManager extends RelationManager
 | 
			
		||||
                            ->options(collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
 | 
			
		||||
                            ->label(trans('admin/server.ip_address'))
 | 
			
		||||
                            ->inlineLabel()
 | 
			
		||||
                            ->ipv4()
 | 
			
		||||
                            ->ip()
 | 
			
		||||
                            ->live()
 | 
			
		||||
                            ->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
 | 
			
		||||
                            ->required(),
 | 
			
		||||
                        TextInput::make('allocation_alias')
 | 
			
		||||
@ -81,9 +91,8 @@ class AllocationsRelationManager extends RelationManager
 | 
			
		||||
                            ->label(trans('admin/server.ports'))
 | 
			
		||||
                            ->inlineLabel()
 | 
			
		||||
                            ->live()
 | 
			
		||||
                            ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
 | 
			
		||||
                                CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))
 | 
			
		||||
                            )
 | 
			
		||||
                            ->disabled(fn (Get $get) => empty($get('allocation_ip')))
 | 
			
		||||
                            ->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))))
 | 
			
		||||
                            ->splitKeys(['Tab', ' ', ','])
 | 
			
		||||
                            ->required(),
 | 
			
		||||
                    ])
 | 
			
		||||
@ -96,10 +105,21 @@ class AllocationsRelationManager extends RelationManager
 | 
			
		||||
                    ->recordSelectSearchColumns(['ip', 'port'])
 | 
			
		||||
                    ->label(trans('admin/server.add_allocation')),
 | 
			
		||||
            ])
 | 
			
		||||
            ->bulkActions([
 | 
			
		||||
                Tables\Actions\BulkActionGroup::make([
 | 
			
		||||
                    Tables\Actions\DissociateBulkAction::make(),
 | 
			
		||||
                ]),
 | 
			
		||||
            ->groupedBulkActions([
 | 
			
		||||
                DissociateBulkAction::make()
 | 
			
		||||
                    ->before(function (DissociateBulkAction $action, Collection $records) {
 | 
			
		||||
                        $records = $records->filter(function ($allocation) {
 | 
			
		||||
                            /** @var Allocation $allocation */
 | 
			
		||||
                            return $allocation->id !== $this->getOwnerRecord()->allocation_id;
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        if ($records->isEmpty()) {
 | 
			
		||||
                            $action->failureNotificationTitle(trans('admin/server.notifications.dissociate_primary'))->failure();
 | 
			
		||||
                            throw new Halt();
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return $records;
 | 
			
		||||
                    }),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ use App\Filament\Admin\Resources\UserResource\Pages;
 | 
			
		||||
use App\Filament\Admin\Resources\UserResource\RelationManagers;
 | 
			
		||||
use App\Models\Role;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Forms\Components\CheckboxList;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Form;
 | 
			
		||||
@ -17,6 +18,7 @@ use Filament\Tables\Columns\IconColumn;
 | 
			
		||||
use Filament\Tables\Columns\ImageColumn;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
 | 
			
		||||
class UserResource extends Resource
 | 
			
		||||
{
 | 
			
		||||
@ -43,7 +45,7 @@ class UserResource extends Resource
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationGroup(): ?string
 | 
			
		||||
    {
 | 
			
		||||
        return trans('admin/dashboard.user');
 | 
			
		||||
        return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.user');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNavigationBadge(): ?string
 | 
			
		||||
@ -58,8 +60,9 @@ class UserResource extends Resource
 | 
			
		||||
                ImageColumn::make('picture')
 | 
			
		||||
                    ->visibleFrom('lg')
 | 
			
		||||
                    ->label('')
 | 
			
		||||
                    ->extraImgAttributes(['class' => 'rounded-full'])
 | 
			
		||||
                    ->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
 | 
			
		||||
                    ->circular()
 | 
			
		||||
                    ->alignCenter()
 | 
			
		||||
                    ->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)),
 | 
			
		||||
                TextColumn::make('username')
 | 
			
		||||
                    ->label(trans('admin/user.username')),
 | 
			
		||||
                TextColumn::make('email')
 | 
			
		||||
@ -120,17 +123,26 @@ class UserResource extends Resource
 | 
			
		||||
                    ->hintIconTooltip(fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
 | 
			
		||||
                    ->password(),
 | 
			
		||||
                CheckboxList::make('roles')
 | 
			
		||||
                    ->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
 | 
			
		||||
                    ->relationship('roles', 'name')
 | 
			
		||||
                    ->saveRelationshipsUsing(function (User $user, array $state) {
 | 
			
		||||
                        $roles = collect($state)->map(fn ($role) => Role::findById($role))->filter(fn ($role) => $role->id !== Role::getRootAdmin()->id);
 | 
			
		||||
 | 
			
		||||
                        $user->syncRoles($roles);
 | 
			
		||||
                    })
 | 
			
		||||
                    ->hidden(fn (User $user) => $user->isRootAdmin())
 | 
			
		||||
                    ->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
 | 
			
		||||
                    ->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
 | 
			
		||||
                    ->dehydrated()
 | 
			
		||||
                    ->label(trans('admin/user.admin_roles'))
 | 
			
		||||
                    ->columnSpanFull()
 | 
			
		||||
                    ->bulkToggleable(false),
 | 
			
		||||
                CheckboxList::make('root_admin_role')
 | 
			
		||||
                    ->visible(fn (User $user) => $user->isRootAdmin())
 | 
			
		||||
                    ->disabled()
 | 
			
		||||
                    ->options([
 | 
			
		||||
                        'root_admin' => Role::ROOT_ADMIN,
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->descriptions([
 | 
			
		||||
                        'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->formatStateUsing(fn () => ['root_admin'])
 | 
			
		||||
                    ->dehydrated(false)
 | 
			
		||||
                    ->label(trans('admin/user.admin_roles'))
 | 
			
		||||
                    ->columnSpanFull(),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,13 @@ class CreateUser extends CreateRecord
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function prepareForValidation($attributes): array
 | 
			
		||||
    {
 | 
			
		||||
        $attributes['data']['email'] = mb_strtolower($attributes['data']['email']);
 | 
			
		||||
 | 
			
		||||
        return $attributes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function handleRecordCreation(array $data): Model
 | 
			
		||||
    {
 | 
			
		||||
        $data['root_admin'] = false;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								app/Filament/Admin/Widgets/CanaryWidget.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/Filament/Admin/Widgets/CanaryWidget.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Widgets;
 | 
			
		||||
 | 
			
		||||
use Filament\Actions\CreateAction;
 | 
			
		||||
use Filament\Widgets\Widget;
 | 
			
		||||
 | 
			
		||||
class CanaryWidget extends Widget
 | 
			
		||||
{
 | 
			
		||||
    protected static string $view = 'filament.admin.widgets.canary-widget';
 | 
			
		||||
 | 
			
		||||
    protected static bool $isLazy = false;
 | 
			
		||||
 | 
			
		||||
    protected static ?int $sort = 1;
 | 
			
		||||
 | 
			
		||||
    public static function canView(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return config('app.version') === 'canary';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getViewData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'actions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
 | 
			
		||||
                    ->icon('tabler-brand-github')
 | 
			
		||||
                    ->url('https://github.com/pelican-dev/panel/issues', true),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								app/Filament/Admin/Widgets/HelpWidget.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/Filament/Admin/Widgets/HelpWidget.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Widgets;
 | 
			
		||||
 | 
			
		||||
use Filament\Actions\CreateAction;
 | 
			
		||||
use Filament\Widgets\Widget;
 | 
			
		||||
 | 
			
		||||
class HelpWidget extends Widget
 | 
			
		||||
{
 | 
			
		||||
    protected static string $view = 'filament.admin.widgets.help-widget';
 | 
			
		||||
 | 
			
		||||
    protected static bool $isLazy = false;
 | 
			
		||||
 | 
			
		||||
    protected static ?int $sort = 4;
 | 
			
		||||
 | 
			
		||||
    public function getViewData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'actions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-help.button_docs'))
 | 
			
		||||
                    ->icon('tabler-speedboat')
 | 
			
		||||
                    ->url('https://pelican.dev/docs', true),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								app/Filament/Admin/Widgets/NoNodesWidget.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/Filament/Admin/Widgets/NoNodesWidget.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Widgets;
 | 
			
		||||
 | 
			
		||||
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
 | 
			
		||||
use App\Models\Node;
 | 
			
		||||
use Filament\Actions\CreateAction;
 | 
			
		||||
use Filament\Widgets\Widget;
 | 
			
		||||
 | 
			
		||||
class NoNodesWidget extends Widget
 | 
			
		||||
{
 | 
			
		||||
    protected static string $view = 'filament.admin.widgets.no-nodes-widget';
 | 
			
		||||
 | 
			
		||||
    protected static bool $isLazy = false;
 | 
			
		||||
 | 
			
		||||
    protected static ?int $sort = 2;
 | 
			
		||||
 | 
			
		||||
    public static function canView(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return Node::count() <= 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getViewData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'actions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-first-node.button_label'))
 | 
			
		||||
                    ->icon('tabler-server-2')
 | 
			
		||||
                    ->url(CreateNode::getUrl()),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								app/Filament/Admin/Widgets/SupportWidget.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/Filament/Admin/Widgets/SupportWidget.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Widgets;
 | 
			
		||||
 | 
			
		||||
use Filament\Actions\CreateAction;
 | 
			
		||||
use Filament\Widgets\Widget;
 | 
			
		||||
 | 
			
		||||
class SupportWidget extends Widget
 | 
			
		||||
{
 | 
			
		||||
    protected static string $view = 'filament.admin.widgets.support-widget';
 | 
			
		||||
 | 
			
		||||
    protected static bool $isLazy = false;
 | 
			
		||||
 | 
			
		||||
    protected static ?int $sort = 3;
 | 
			
		||||
 | 
			
		||||
    public function getViewData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'actions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-support.button_donate'))
 | 
			
		||||
                    ->icon('tabler-cash')
 | 
			
		||||
                    ->url('https://pelican.dev/donate', true)
 | 
			
		||||
                    ->color('success'),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								app/Filament/Admin/Widgets/UpdateWidget.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/Filament/Admin/Widgets/UpdateWidget.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Admin\Widgets;
 | 
			
		||||
 | 
			
		||||
use App\Services\Helpers\SoftwareVersionService;
 | 
			
		||||
use Filament\Actions\CreateAction;
 | 
			
		||||
use Filament\Widgets\Widget;
 | 
			
		||||
 | 
			
		||||
class UpdateWidget extends Widget
 | 
			
		||||
{
 | 
			
		||||
    protected static string $view = 'filament.admin.widgets.update-widget';
 | 
			
		||||
 | 
			
		||||
    protected static bool $isLazy = false;
 | 
			
		||||
 | 
			
		||||
    protected static ?int $sort = 0;
 | 
			
		||||
 | 
			
		||||
    private SoftwareVersionService $softwareVersionService;
 | 
			
		||||
 | 
			
		||||
    public function mount(SoftwareVersionService $softwareVersionService): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->softwareVersionService = $softwareVersionService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getViewData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            'version' => $this->softwareVersionService->currentPanelVersion(),
 | 
			
		||||
            'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
 | 
			
		||||
            'isLatest' => $this->softwareVersionService->isLatestPanel(),
 | 
			
		||||
            'actions' => [
 | 
			
		||||
                CreateAction::make()
 | 
			
		||||
                    ->label(trans('admin/dashboard.sections.intro-update-available.heading'))
 | 
			
		||||
                    ->icon('tabler-clipboard-text')
 | 
			
		||||
                    ->url('https://pelican.dev/docs/panel/update', true)
 | 
			
		||||
                    ->color('warning'),
 | 
			
		||||
            ],
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2,52 +2,179 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\App\Resources\ServerResource\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Enums\ServerResourceType;
 | 
			
		||||
use App\Filament\App\Resources\ServerResource;
 | 
			
		||||
use App\Filament\Components\Tables\Columns\ServerEntryColumn;
 | 
			
		||||
use App\Filament\Server\Pages\Console;
 | 
			
		||||
use App\Models\Permission;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Repositories\Daemon\DaemonPowerRepository;
 | 
			
		||||
use AymanAlhattami\FilamentContextMenu\Columns\ContextMenuTextColumn;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Filament\Resources\Components\Tab;
 | 
			
		||||
use Filament\Resources\Pages\ListRecords;
 | 
			
		||||
use Filament\Tables\Actions\Action;
 | 
			
		||||
use Filament\Tables\Columns\ColumnGroup;
 | 
			
		||||
use Filament\Tables\Columns\Layout\Stack;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Filters\SelectFilter;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Http\Client\ConnectionException;
 | 
			
		||||
use Livewire\Attributes\On;
 | 
			
		||||
 | 
			
		||||
class ListServers extends ListRecords
 | 
			
		||||
{
 | 
			
		||||
    protected static string $resource = ServerResource::class;
 | 
			
		||||
 | 
			
		||||
    public const DANGER_THRESHOLD = 0.9;
 | 
			
		||||
 | 
			
		||||
    public const WARNING_THRESHOLD = 0.7;
 | 
			
		||||
 | 
			
		||||
    private DaemonPowerRepository $daemonPowerRepository;
 | 
			
		||||
 | 
			
		||||
    public function boot(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->daemonPowerRepository = new DaemonPowerRepository();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function table(Table $table): Table
 | 
			
		||||
    {
 | 
			
		||||
        $baseQuery = auth()->user()->accessibleServers();
 | 
			
		||||
 | 
			
		||||
        $menuOptions = function (Server $server) {
 | 
			
		||||
            $status = $server->retrieveStatus();
 | 
			
		||||
 | 
			
		||||
            return [
 | 
			
		||||
                Action::make('start')
 | 
			
		||||
                    ->color('primary')
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
 | 
			
		||||
                    ->visible(fn () => $status->isStartable())
 | 
			
		||||
                    ->dispatch('powerAction', ['server' => $server, 'action' => 'start'])
 | 
			
		||||
                    ->icon('tabler-player-play-filled'),
 | 
			
		||||
                Action::make('restart')
 | 
			
		||||
                    ->color('gray')
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
 | 
			
		||||
                    ->visible(fn () => $status->isRestartable())
 | 
			
		||||
                    ->dispatch('powerAction', ['server' => $server, 'action' => 'restart'])
 | 
			
		||||
                    ->icon('tabler-refresh'),
 | 
			
		||||
                Action::make('stop')
 | 
			
		||||
                    ->color('danger')
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
 | 
			
		||||
                    ->visible(fn () => $status->isStoppable())
 | 
			
		||||
                    ->dispatch('powerAction', ['server' => $server, 'action' => 'stop'])
 | 
			
		||||
                    ->icon('tabler-player-stop-filled'),
 | 
			
		||||
                Action::make('kill')
 | 
			
		||||
                    ->color('danger')
 | 
			
		||||
                    ->tooltip('This can result in data corruption and/or data loss!')
 | 
			
		||||
                    ->dispatch('powerAction', ['server' => $server, 'action' => 'kill'])
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
 | 
			
		||||
                    ->visible(fn () => $status->isKillable())
 | 
			
		||||
                    ->icon('tabler-alert-square'),
 | 
			
		||||
            ];
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $viewOne = [
 | 
			
		||||
            ContextMenuTextColumn::make('condition')
 | 
			
		||||
                ->label('')
 | 
			
		||||
                ->default('unknown')
 | 
			
		||||
                ->wrap()
 | 
			
		||||
                ->badge()
 | 
			
		||||
                ->alignCenter()
 | 
			
		||||
                ->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
 | 
			
		||||
                ->icon(fn (Server $server) => $server->condition->getIcon())
 | 
			
		||||
                ->color(fn (Server $server) => $server->condition->getColor())
 | 
			
		||||
                ->contextMenuActions($menuOptions)
 | 
			
		||||
                ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $viewTwo = [
 | 
			
		||||
            ContextMenuTextColumn::make('name')
 | 
			
		||||
                ->label('')
 | 
			
		||||
                ->size('md')
 | 
			
		||||
                ->searchable()
 | 
			
		||||
                ->contextMenuActions($menuOptions)
 | 
			
		||||
                ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
 | 
			
		||||
            ContextMenuTextColumn::make('allocation.address')
 | 
			
		||||
                ->label('')
 | 
			
		||||
                ->badge()
 | 
			
		||||
                ->copyable(request()->isSecure())
 | 
			
		||||
                ->contextMenuActions($menuOptions)
 | 
			
		||||
                ->enableContextMenu(fn (Server $server) => !$server->isInConflictState()),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $viewThree = [
 | 
			
		||||
            TextColumn::make('cpuUsage')
 | 
			
		||||
                ->label('')
 | 
			
		||||
                ->icon('tabler-cpu')
 | 
			
		||||
                ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0))
 | 
			
		||||
                ->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage))
 | 
			
		||||
                ->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
 | 
			
		||||
            TextColumn::make('memoryUsage')
 | 
			
		||||
                ->label('')
 | 
			
		||||
                ->icon('tabler-memory')
 | 
			
		||||
                ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true))
 | 
			
		||||
                ->state(fn (Server $server) => $server->formatResource('memory_bytes'))
 | 
			
		||||
                ->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
 | 
			
		||||
            TextColumn::make('diskUsage')
 | 
			
		||||
                ->label('')
 | 
			
		||||
                ->icon('tabler-device-floppy')
 | 
			
		||||
                ->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true))
 | 
			
		||||
                ->state(fn (Server $server) => $server->formatResource('disk_bytes'))
 | 
			
		||||
                ->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        return $table
 | 
			
		||||
            ->paginated(false)
 | 
			
		||||
            ->query(fn () => $baseQuery)
 | 
			
		||||
            ->poll('15s')
 | 
			
		||||
            ->columns([
 | 
			
		||||
                Stack::make([
 | 
			
		||||
                    ServerEntryColumn::make('server_entry')
 | 
			
		||||
                        ->searchable(['name']),
 | 
			
		||||
                ]),
 | 
			
		||||
            ])
 | 
			
		||||
            ->columns(
 | 
			
		||||
                (auth()->user()->getCustomization()['dashboard_layout'] ?? 'grid') === 'grid'
 | 
			
		||||
                    ? [
 | 
			
		||||
                        Stack::make([
 | 
			
		||||
                            ServerEntryColumn::make('server_entry')
 | 
			
		||||
                                ->searchable(['name']),
 | 
			
		||||
                        ]),
 | 
			
		||||
                    ]
 | 
			
		||||
                    : [
 | 
			
		||||
                        ColumnGroup::make('Status')
 | 
			
		||||
                            ->label('Status')
 | 
			
		||||
                            ->columns($viewOne),
 | 
			
		||||
                        ColumnGroup::make('Server')
 | 
			
		||||
                            ->label('Servers')
 | 
			
		||||
                            ->columns($viewTwo),
 | 
			
		||||
                        ColumnGroup::make('Resources')
 | 
			
		||||
                            ->label('Resources')
 | 
			
		||||
                            ->columns($viewThree),
 | 
			
		||||
                    ]
 | 
			
		||||
            )
 | 
			
		||||
            ->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
 | 
			
		||||
            ->contentGrid([
 | 
			
		||||
                'default' => 1,
 | 
			
		||||
                'md' => 2,
 | 
			
		||||
            ])
 | 
			
		||||
            ->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
 | 
			
		||||
            ->emptyStateIcon('tabler-brand-docker')
 | 
			
		||||
            ->emptyStateDescription('')
 | 
			
		||||
            ->emptyStateHeading('You don\'t have access to any servers!')
 | 
			
		||||
            ->emptyStateHeading(fn () => $this->activeTab === 'my' ? 'You don\'t own any servers!' : 'You don\'t have access to any servers!')
 | 
			
		||||
            ->persistFiltersInSession()
 | 
			
		||||
            ->filters([
 | 
			
		||||
                SelectFilter::make('egg')
 | 
			
		||||
                    ->relationship('egg', 'name', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('egg_id')))
 | 
			
		||||
                    ->searchable()
 | 
			
		||||
                    ->preload(),
 | 
			
		||||
                SelectFilter::make('owner')
 | 
			
		||||
                    ->relationship('user', 'username', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('owner_id')))
 | 
			
		||||
                    ->searchable()
 | 
			
		||||
                    ->hidden(fn () => $this->activeTab === 'my')
 | 
			
		||||
                    ->preload(),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function updatedActiveTab(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->resetTable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTabs(): array
 | 
			
		||||
    {
 | 
			
		||||
        $all = auth()->user()->accessibleServers();
 | 
			
		||||
@ -67,4 +194,71 @@ class ListServers extends ListRecords
 | 
			
		||||
                ->badge($all->count()),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getResourceColor(Server $server, string $resource): ?string
 | 
			
		||||
    {
 | 
			
		||||
        $current = null;
 | 
			
		||||
        $limit = null;
 | 
			
		||||
 | 
			
		||||
        switch ($resource) {
 | 
			
		||||
            case 'cpu':
 | 
			
		||||
                $current = $server->resources()['cpu_absolute'] ?? 0;
 | 
			
		||||
                $limit = $server->cpu;
 | 
			
		||||
                if ($server->cpu === 0) {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'memory':
 | 
			
		||||
                $current = $server->resources()['memory_bytes'] ?? 0;
 | 
			
		||||
                $limit = $server->memory * 2 ** 20;
 | 
			
		||||
                if ($server->memory === 0) {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'disk':
 | 
			
		||||
                $current = $server->resources()['disk_bytes'] ?? 0;
 | 
			
		||||
                $limit = $server->disk * 2 ** 20;
 | 
			
		||||
                if ($server->disk === 0) {
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($current >= $limit * self::DANGER_THRESHOLD) {
 | 
			
		||||
            return 'danger';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($current >= $limit * self::WARNING_THRESHOLD) {
 | 
			
		||||
            return 'warning';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[On('powerAction')]
 | 
			
		||||
    public function powerAction(Server $server, string $action): void
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $this->daemonPowerRepository->setServer($server)->send($action);
 | 
			
		||||
 | 
			
		||||
            Notification::make()
 | 
			
		||||
                ->title('Power Action')
 | 
			
		||||
                ->body($action . ' sent to ' . $server->name)
 | 
			
		||||
                ->success()
 | 
			
		||||
                ->send();
 | 
			
		||||
 | 
			
		||||
            $this->redirect(self::getUrl(['activeTab' => $this->activeTab]));
 | 
			
		||||
        } catch (ConnectionException) {
 | 
			
		||||
            Notification::make()
 | 
			
		||||
                ->title(trans('exceptions.node.error_connecting', ['node' => $server->node->name]))
 | 
			
		||||
                ->danger()
 | 
			
		||||
                ->send();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,12 @@ class CopyFrom extends Select
 | 
			
		||||
 | 
			
		||||
        $this->placeholder(trans('admin/egg.none'));
 | 
			
		||||
 | 
			
		||||
        $this->preload();
 | 
			
		||||
 | 
			
		||||
        $this->searchable();
 | 
			
		||||
 | 
			
		||||
        $this->native(false);
 | 
			
		||||
 | 
			
		||||
        $this->live();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ use chillerlan\QRCode\QROptions;
 | 
			
		||||
use DateTimeZone;
 | 
			
		||||
use Filament\Forms\Components\Actions;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action;
 | 
			
		||||
use Filament\Forms\Components\FileUpload;
 | 
			
		||||
use Filament\Forms\Components\Grid;
 | 
			
		||||
use Filament\Forms\Components\Placeholder;
 | 
			
		||||
use Filament\Forms\Components\Repeater;
 | 
			
		||||
@ -29,6 +30,7 @@ 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\Get;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Filament\Pages\Auth\EditProfile as BaseEditProfile;
 | 
			
		||||
@ -38,6 +40,7 @@ use Filament\Support\Exceptions\Halt;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use Illuminate\Support\Facades\Hash;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
use Illuminate\Validation\Rules\Password;
 | 
			
		||||
use Laravel\Socialite\Facades\Socialite;
 | 
			
		||||
@ -125,6 +128,21 @@ class EditProfile extends BaseEditProfile
 | 
			
		||||
                                            ->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state])))
 | 
			
		||||
                                            ->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
 | 
			
		||||
                                            ->native(false),
 | 
			
		||||
                                        FileUpload::make('avatar')
 | 
			
		||||
                                            ->visible(fn () => config('panel.filament.uploadable-avatars'))
 | 
			
		||||
                                            ->avatar()
 | 
			
		||||
                                            ->acceptedFileTypes(['image/png'])
 | 
			
		||||
                                            ->directory('avatars')
 | 
			
		||||
                                            ->getUploadedFileNameForStorageUsing(fn () => $this->getUser()->id . '.png')
 | 
			
		||||
                                            ->hintAction(function (FileUpload $fileUpload) {
 | 
			
		||||
                                                $path = $fileUpload->getDirectory() . '/' . $this->getUser()->id . '.png';
 | 
			
		||||
 | 
			
		||||
                                                return Action::make('remove_avatar')
 | 
			
		||||
                                                    ->icon('tabler-photo-minus')
 | 
			
		||||
                                                    ->iconButton()
 | 
			
		||||
                                                    ->hidden(fn () => !$fileUpload->getDisk()->exists($path))
 | 
			
		||||
                                                    ->action(fn () => $fileUpload->getDisk()->delete($path));
 | 
			
		||||
                                            }),
 | 
			
		||||
                                    ]),
 | 
			
		||||
 | 
			
		||||
                                Tab::make(trans('profile.tabs.oauth'))
 | 
			
		||||
@ -242,6 +260,7 @@ class EditProfile extends BaseEditProfile
 | 
			
		||||
                                                ->password(),
 | 
			
		||||
                                        ];
 | 
			
		||||
                                    }),
 | 
			
		||||
 | 
			
		||||
                                Tab::make(trans('profile.tabs.api_keys'))
 | 
			
		||||
                                    ->icon('tabler-key')
 | 
			
		||||
                                    ->schema([
 | 
			
		||||
@ -261,7 +280,7 @@ class EditProfile extends BaseEditProfile
 | 
			
		||||
                                                Action::make('Create')
 | 
			
		||||
                                                    ->label(trans('filament-actions::create.single.modal.actions.create.label'))
 | 
			
		||||
                                                    ->disabled(fn (Get $get) => $get('description') === null)
 | 
			
		||||
                                                    ->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab']))
 | 
			
		||||
                                                    ->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'], panel: 'app'))
 | 
			
		||||
                                                    ->action(function (Get $get, Action $action, User $user) {
 | 
			
		||||
                                                        $token = $user->createToken(
 | 
			
		||||
                                                            $get('description'),
 | 
			
		||||
@ -308,9 +327,11 @@ class EditProfile extends BaseEditProfile
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    ]),
 | 
			
		||||
 | 
			
		||||
                                Tab::make(trans('profile.tabs.ssh_keys'))
 | 
			
		||||
                                    ->icon('tabler-lock-code')
 | 
			
		||||
                                    ->hidden(),
 | 
			
		||||
 | 
			
		||||
                                Tab::make(trans('profile.tabs.activity'))
 | 
			
		||||
                                    ->icon('tabler-history')
 | 
			
		||||
                                    ->schema([
 | 
			
		||||
@ -325,6 +346,105 @@ class EditProfile extends BaseEditProfile
 | 
			
		||||
                                                Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                    ]),
 | 
			
		||||
 | 
			
		||||
                                Tab::make(trans('profile.tabs.customization'))
 | 
			
		||||
                                    ->icon('tabler-adjustments')
 | 
			
		||||
                                    ->schema([
 | 
			
		||||
                                        Section::make(trans('profile.dashboard'))
 | 
			
		||||
                                            ->collapsible()
 | 
			
		||||
                                            ->icon('tabler-dashboard')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                ToggleButtons::make('dashboard_layout')
 | 
			
		||||
                                                    ->label(trans('profile.dashboard_layout'))
 | 
			
		||||
                                                    ->inline()
 | 
			
		||||
                                                    ->required()
 | 
			
		||||
                                                    ->options([
 | 
			
		||||
                                                        'grid' => trans('profile.grid'),
 | 
			
		||||
                                                        'table' => trans('profile.table'),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Section::make(trans('profile.console'))
 | 
			
		||||
                                            ->collapsible()
 | 
			
		||||
                                            ->icon('tabler-brand-tabler')
 | 
			
		||||
                                            ->columns(4)
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                TextInput::make('console_font_size')
 | 
			
		||||
                                                    ->label(trans('profile.font_size'))
 | 
			
		||||
                                                    ->columnSpan(1)
 | 
			
		||||
                                                    ->minValue(1)
 | 
			
		||||
                                                    ->numeric()
 | 
			
		||||
                                                    ->required()
 | 
			
		||||
                                                    ->default(14),
 | 
			
		||||
                                                Select::make('console_font')
 | 
			
		||||
                                                    ->label(trans('profile.font'))
 | 
			
		||||
                                                    ->required()
 | 
			
		||||
                                                    ->options(function () {
 | 
			
		||||
                                                        $fonts = [
 | 
			
		||||
                                                            'monospace' => 'monospace', //default
 | 
			
		||||
                                                        ];
 | 
			
		||||
 | 
			
		||||
                                                        if (!Storage::disk('public')->exists('fonts')) {
 | 
			
		||||
                                                            Storage::disk('public')->makeDirectory('fonts');
 | 
			
		||||
                                                            $this->fillForm();
 | 
			
		||||
                                                        }
 | 
			
		||||
 | 
			
		||||
                                                        foreach (Storage::disk('public')->allFiles('fonts') as $file) {
 | 
			
		||||
                                                            $fileInfo = pathinfo($file);
 | 
			
		||||
 | 
			
		||||
                                                            if ($fileInfo['extension'] === 'ttf') {
 | 
			
		||||
                                                                $fonts[$fileInfo['filename']] = $fileInfo['filename'];
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                        }
 | 
			
		||||
 | 
			
		||||
                                                        return $fonts;
 | 
			
		||||
                                                    })
 | 
			
		||||
                                                    ->reactive()
 | 
			
		||||
                                                    ->default('monospace')
 | 
			
		||||
                                                    ->afterStateUpdated(fn ($state, callable $set) => $set('font_preview', $state)),
 | 
			
		||||
                                                Placeholder::make('font_preview')
 | 
			
		||||
                                                    ->label(trans('profile.font_preview'))
 | 
			
		||||
                                                    ->columnSpan(2)
 | 
			
		||||
                                                    ->content(function (Get $get) {
 | 
			
		||||
                                                        $fontName = $get('console_font') ?? 'monospace';
 | 
			
		||||
                                                        $fontSize = $get('console_font_size') . 'px';
 | 
			
		||||
                                                        $fontUrl = asset("storage/fonts/{$fontName}.ttf");
 | 
			
		||||
 | 
			
		||||
                                                        return new HtmlString(<<<HTML
 | 
			
		||||
                                                                    <style>
 | 
			
		||||
                                                                        @font-face {
 | 
			
		||||
                                                                            font-family: "CustomPreviewFont";
 | 
			
		||||
                                                                            src: url("$fontUrl");
 | 
			
		||||
                                                                        }
 | 
			
		||||
                                                                        .preview-text {
 | 
			
		||||
                                                                            font-family: "CustomPreviewFont";
 | 
			
		||||
                                                                            font-size: $fontSize;
 | 
			
		||||
                                                                            margin-top: 10px;
 | 
			
		||||
                                                                            display: block;
 | 
			
		||||
                                                                        }
 | 
			
		||||
                                                                    </style>
 | 
			
		||||
                                                                    <span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
 | 
			
		||||
                                                                HTML);
 | 
			
		||||
                                                    }),
 | 
			
		||||
                                                TextInput::make('console_graph_period')
 | 
			
		||||
                                                    ->label(trans('profile.graph_period'))
 | 
			
		||||
                                                    ->suffix(trans('profile.seconds'))
 | 
			
		||||
                                                    ->hintIcon('tabler-question-mark')
 | 
			
		||||
                                                    ->hintIconTooltip(trans('profile.graph_period_helper'))
 | 
			
		||||
                                                    ->columnSpan(2)
 | 
			
		||||
                                                    ->numeric()
 | 
			
		||||
                                                    ->default(30)
 | 
			
		||||
                                                    ->minValue(10)
 | 
			
		||||
                                                    ->maxValue(120)
 | 
			
		||||
                                                    ->required(),
 | 
			
		||||
                                                TextInput::make('console_rows')
 | 
			
		||||
                                                    ->label(trans('profile.rows'))
 | 
			
		||||
                                                    ->minValue(1)
 | 
			
		||||
                                                    ->numeric()
 | 
			
		||||
                                                    ->required()
 | 
			
		||||
                                                    ->columnSpan(2)
 | 
			
		||||
                                                    ->default(30),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                    ]),
 | 
			
		||||
                            ]),
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->operation('edit')
 | 
			
		||||
@ -345,7 +465,7 @@ class EditProfile extends BaseEditProfile
 | 
			
		||||
            $tokens = $this->toggleTwoFactorService->handle($record, $token, true);
 | 
			
		||||
            cache()->put("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
 | 
			
		||||
 | 
			
		||||
            $this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
 | 
			
		||||
            $this->redirect(self::getUrl(['tab' => '-2fa-tab'], panel: 'app'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($token = $data['2fa-disable-code'] ?? null) {
 | 
			
		||||
@ -381,4 +501,33 @@ class EditProfile extends BaseEditProfile
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function mutateFormDataBeforeSave(array $data): array
 | 
			
		||||
    {
 | 
			
		||||
        $moarbetterdata = [
 | 
			
		||||
            'console_font' => $data['console_font'],
 | 
			
		||||
            'console_font_size' => $data['console_font_size'],
 | 
			
		||||
            'console_rows' => $data['console_rows'],
 | 
			
		||||
            'console_graph_period' => $data['console_graph_period'],
 | 
			
		||||
            'dashboard_layout' => $data['dashboard_layout'],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout']);
 | 
			
		||||
        $data['customization'] = json_encode($moarbetterdata);
 | 
			
		||||
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function mutateFormDataBeforeFill(array $data): array
 | 
			
		||||
    {
 | 
			
		||||
        $moarbetterdata = json_decode($data['customization'], true);
 | 
			
		||||
 | 
			
		||||
        $data['console_font'] = $moarbetterdata['console_font'] ?? 'monospace';
 | 
			
		||||
        $data['console_font_size'] = $moarbetterdata['console_font_size'] ?? 14;
 | 
			
		||||
        $data['console_rows'] = $moarbetterdata['console_rows'] ?? 30;
 | 
			
		||||
        $data['console_graph_period'] = $moarbetterdata['console_graph_period'] ?? 30;
 | 
			
		||||
        $data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid';
 | 
			
		||||
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -57,11 +57,22 @@ class Login extends BaseLogin
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $isValidToken = $this->google2FA->verifyKey(
 | 
			
		||||
            $user->totp_secret,
 | 
			
		||||
            $token,
 | 
			
		||||
            Config::integer('panel.auth.2fa.window'),
 | 
			
		||||
        );
 | 
			
		||||
        $isValidToken = false;
 | 
			
		||||
        if (strlen($token) === $this->google2FA->getOneTimePasswordLength()) {
 | 
			
		||||
            $isValidToken = $this->google2FA->verifyKey(
 | 
			
		||||
                $user->totp_secret,
 | 
			
		||||
                $token,
 | 
			
		||||
                Config::integer('panel.auth.2fa.window'),
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            foreach ($user->recoveryTokens as $recoveryToken) {
 | 
			
		||||
                if (password_verify($token, $recoveryToken->token)) {
 | 
			
		||||
                    $isValidToken = true;
 | 
			
		||||
                    $recoveryToken->delete();
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$isValidToken) {
 | 
			
		||||
            // Buffer to prevent bruteforce
 | 
			
		||||
@ -108,7 +119,9 @@ class Login extends BaseLogin
 | 
			
		||||
    {
 | 
			
		||||
        return TextInput::make('2fa')
 | 
			
		||||
            ->label(trans('auth.two-factor-code'))
 | 
			
		||||
            ->hidden(fn () => !$this->verifyTwoFactor)
 | 
			
		||||
            ->hintIcon('tabler-question-mark')
 | 
			
		||||
            ->hintIconTooltip(trans('auth.two-factor-hint'))
 | 
			
		||||
            ->visible(fn () => $this->verifyTwoFactor)
 | 
			
		||||
            ->required()
 | 
			
		||||
            ->live();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -2,18 +2,21 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Server\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Enums\ConsoleWidgetPosition;
 | 
			
		||||
use App\Enums\ContainerStatus;
 | 
			
		||||
use App\Exceptions\Http\Server\ServerStateConflictException;
 | 
			
		||||
use App\Extensions\Features\FeatureProvider;
 | 
			
		||||
use App\Filament\Server\Widgets\ServerConsole;
 | 
			
		||||
use App\Filament\Server\Widgets\ServerCpuChart;
 | 
			
		||||
use App\Filament\Server\Widgets\ServerMemoryChart;
 | 
			
		||||
// use App\Filament\Server\Widgets\ServerNetworkChart;
 | 
			
		||||
use App\Filament\Server\Widgets\ServerNetworkChart;
 | 
			
		||||
use App\Filament\Server\Widgets\ServerOverview;
 | 
			
		||||
use App\Livewire\AlertBanner;
 | 
			
		||||
use App\Models\Permission;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Filament\Actions\Action;
 | 
			
		||||
use Filament\Actions\Concerns\InteractsWithActions;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Actions\Action;
 | 
			
		||||
use Filament\Pages\Page;
 | 
			
		||||
use Filament\Support\Enums\ActionSize;
 | 
			
		||||
use Filament\Widgets\Widget;
 | 
			
		||||
@ -22,6 +25,8 @@ use Livewire\Attributes\On;
 | 
			
		||||
 | 
			
		||||
class Console extends Page
 | 
			
		||||
{
 | 
			
		||||
    use InteractsWithActions;
 | 
			
		||||
 | 
			
		||||
    protected static ?string $navigationIcon = 'tabler-brand-tabler';
 | 
			
		||||
 | 
			
		||||
    protected static ?int $navigationSort = 1;
 | 
			
		||||
@ -38,14 +43,38 @@ class Console extends Page
 | 
			
		||||
        try {
 | 
			
		||||
            $server->validateCurrentState();
 | 
			
		||||
        } catch (ServerStateConflictException $exception) {
 | 
			
		||||
            AlertBanner::make()
 | 
			
		||||
                ->warning()
 | 
			
		||||
            AlertBanner::make('server_conflict')
 | 
			
		||||
                ->title('Warning')
 | 
			
		||||
                ->body($exception->getMessage())
 | 
			
		||||
                ->warning()
 | 
			
		||||
                ->send();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function boot(): void
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
        /** @var FeatureProvider $feature */
 | 
			
		||||
        foreach ($server->egg->features() as $feature) {
 | 
			
		||||
            $this->cacheAction($feature->getAction());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[On('mount-feature')]
 | 
			
		||||
    public function mountFeature(string $data): void
 | 
			
		||||
    {
 | 
			
		||||
        $data = json_decode($data);
 | 
			
		||||
        $feature = data_get($data, 'key');
 | 
			
		||||
 | 
			
		||||
        $feature = FeatureProvider::getProviders($feature);
 | 
			
		||||
        if ($this->getMountedAction()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        $this->mountAction($feature->getId());
 | 
			
		||||
        sleep(2); // TODO find a better way
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getWidgetData(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
@ -54,18 +83,41 @@ class Console extends Page
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @var array<string, array<class-string<Widget>>> */
 | 
			
		||||
    protected static array $customWidgets = [];
 | 
			
		||||
 | 
			
		||||
    /** @param class-string<Widget>[] $customWidgets */
 | 
			
		||||
    public static function registerCustomWidgets(ConsoleWidgetPosition $position, array $customWidgets): void
 | 
			
		||||
    {
 | 
			
		||||
        static::$customWidgets[$position->value] = array_unique(array_merge(static::$customWidgets[$position->value] ?? [], $customWidgets));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return class-string<Widget>[]
 | 
			
		||||
     */
 | 
			
		||||
    public function getWidgets(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            ServerOverview::class,
 | 
			
		||||
            ServerConsole::class,
 | 
			
		||||
        $allWidgets = [];
 | 
			
		||||
 | 
			
		||||
        $allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Top->value] ?? []);
 | 
			
		||||
 | 
			
		||||
        $allWidgets[] = ServerOverview::class;
 | 
			
		||||
 | 
			
		||||
        $allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::AboveConsole->value] ?? []);
 | 
			
		||||
 | 
			
		||||
        $allWidgets[] = ServerConsole::class;
 | 
			
		||||
 | 
			
		||||
        $allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::BelowConsole->value] ?? []);
 | 
			
		||||
 | 
			
		||||
        $allWidgets = array_merge($allWidgets, [
 | 
			
		||||
            ServerCpuChart::class,
 | 
			
		||||
            ServerMemoryChart::class,
 | 
			
		||||
            //ServerNetworkChart::class, TODO: convert units.
 | 
			
		||||
        ];
 | 
			
		||||
            ServerNetworkChart::class,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $allWidgets = array_merge($allWidgets, static::$customWidgets[ConsoleWidgetPosition::Bottom->value] ?? []);
 | 
			
		||||
 | 
			
		||||
        return array_unique($allWidgets);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -102,32 +154,33 @@ class Console extends Page
 | 
			
		||||
            Action::make('start')
 | 
			
		||||
                ->color('primary')
 | 
			
		||||
                ->size(ActionSize::ExtraLarge)
 | 
			
		||||
                ->action(fn () => $this->dispatch('setServerState', state: 'start', uuid: $server->uuid))
 | 
			
		||||
                ->dispatch('setServerState', ['state' => 'start', 'uuid' => $server->uuid])
 | 
			
		||||
                ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
 | 
			
		||||
                ->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable()),
 | 
			
		||||
                ->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable())
 | 
			
		||||
                ->icon('tabler-player-play-filled'),
 | 
			
		||||
            Action::make('restart')
 | 
			
		||||
                ->color('gray')
 | 
			
		||||
                ->size(ActionSize::ExtraLarge)
 | 
			
		||||
                ->action(fn () => $this->dispatch('setServerState', state: 'restart', uuid: $server->uuid))
 | 
			
		||||
                ->dispatch('setServerState', ['state' => 'restart', 'uuid' => $server->uuid])
 | 
			
		||||
                ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
 | 
			
		||||
                ->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable()),
 | 
			
		||||
                ->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable())
 | 
			
		||||
                ->icon('tabler-reload'),
 | 
			
		||||
            Action::make('stop')
 | 
			
		||||
                ->color('danger')
 | 
			
		||||
                ->size(ActionSize::ExtraLarge)
 | 
			
		||||
                ->action(fn () => $this->dispatch('setServerState', state: 'stop', uuid: $server->uuid))
 | 
			
		||||
                ->dispatch('setServerState', ['state' => 'stop', 'uuid' => $server->uuid])
 | 
			
		||||
                ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
 | 
			
		||||
                ->hidden(fn () => $this->status->isStartingOrStopping() || $this->status->isKillable())
 | 
			
		||||
                ->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable()),
 | 
			
		||||
                ->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable())
 | 
			
		||||
                ->icon('tabler-player-stop-filled'),
 | 
			
		||||
            Action::make('kill')
 | 
			
		||||
                ->color('danger')
 | 
			
		||||
                ->requiresConfirmation()
 | 
			
		||||
                ->modalHeading('Do you wish to kill this server?')
 | 
			
		||||
                ->modalDescription('This can result in data corruption and/or data loss!')
 | 
			
		||||
                ->modalSubmitActionLabel('Kill Server')
 | 
			
		||||
                ->tooltip('This can result in data corruption and/or data loss!')
 | 
			
		||||
                ->size(ActionSize::ExtraLarge)
 | 
			
		||||
                ->action(fn () => $this->dispatch('setServerState', state: 'kill', uuid: $server->uuid))
 | 
			
		||||
                ->dispatch('setServerState', ['state' => 'kill', 'uuid' => $server->uuid])
 | 
			
		||||
                ->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
 | 
			
		||||
                ->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable()),
 | 
			
		||||
                ->hidden(fn () => $server->isInConflictState() || !$this->status->isKillable())
 | 
			
		||||
                ->icon('tabler-alert-square'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ use Filament\Forms\Components\Textarea;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Form;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
use Illuminate\Support\Facades\Validator;
 | 
			
		||||
 | 
			
		||||
class Startup extends ServerFormPage
 | 
			
		||||
@ -100,7 +101,7 @@ class Startup extends ServerFormPage
 | 
			
		||||
                    ->schema([
 | 
			
		||||
                        Repeater::make('server_variables')
 | 
			
		||||
                            ->label('')
 | 
			
		||||
                            ->relationship('viewableServerVariables')
 | 
			
		||||
                            ->relationship('serverVariables', fn (Builder $query) => $query->where('egg_variables.user_viewable', true)->orderByPowerJoins('variable.sort'))
 | 
			
		||||
                            ->grid()
 | 
			
		||||
                            ->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
 | 
			
		||||
                            ->reorderable(false)->addable(false)->deletable(false)
 | 
			
		||||
 | 
			
		||||
@ -30,8 +30,7 @@ class ActivityResource extends Resource
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        return $server->activity()
 | 
			
		||||
            ->getQuery()
 | 
			
		||||
        return ActivityLog::whereHas('subjects', fn (Builder $query) => $query->where('subject_id', $server->id))
 | 
			
		||||
            ->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
 | 
			
		||||
            ->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
 | 
			
		||||
                // We could do this with a query and a lot of joins, but that gets pretty
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ use Filament\Tables\Actions\ViewAction;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Filters\SelectFilter;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Illuminate\Support\Arr;
 | 
			
		||||
use Illuminate\Support\HtmlString;
 | 
			
		||||
 | 
			
		||||
class ListActivities extends ListRecords
 | 
			
		||||
@ -31,7 +32,7 @@ class ListActivities extends ListRecords
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        return $table
 | 
			
		||||
            ->paginated([25, 50, 100, 250])
 | 
			
		||||
            ->paginated([25, 50])
 | 
			
		||||
            ->defaultPaginationPageOption(25)
 | 
			
		||||
            ->columns([
 | 
			
		||||
                TextColumn::make('event')
 | 
			
		||||
@ -98,7 +99,7 @@ class ListActivities extends ListRecords
 | 
			
		||||
                        DateTimePicker::make('timestamp'),
 | 
			
		||||
                        KeyValue::make('properties')
 | 
			
		||||
                            ->label('Metadata')
 | 
			
		||||
                            ->formatStateUsing(fn ($state) => collect($state)->filter(fn ($item) => !is_array($item))->all()),
 | 
			
		||||
                            ->formatStateUsing(fn ($state) => Arr::dot($state)),
 | 
			
		||||
                    ]),
 | 
			
		||||
            ])
 | 
			
		||||
            ->filters([
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Server\Resources\BackupResource\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Enums\BackupStatus;
 | 
			
		||||
use App\Enums\ServerState;
 | 
			
		||||
use App\Facades\Activity;
 | 
			
		||||
use App\Filament\Server\Resources\BackupResource;
 | 
			
		||||
@ -70,13 +71,14 @@ class ListBackups extends ListRecords
 | 
			
		||||
                    ->label('Created')
 | 
			
		||||
                    ->since()
 | 
			
		||||
                    ->sortable(),
 | 
			
		||||
                IconColumn::make('is_successful')
 | 
			
		||||
                    ->label('Successful')
 | 
			
		||||
                    ->boolean(),
 | 
			
		||||
                TextColumn::make('status')
 | 
			
		||||
                    ->label('Status')
 | 
			
		||||
                    ->badge(),
 | 
			
		||||
                IconColumn::make('is_locked')
 | 
			
		||||
                    ->visibleFrom('md')
 | 
			
		||||
                    ->label('Lock Status')
 | 
			
		||||
                    ->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock-open' : 'tabler-lock'),
 | 
			
		||||
                    ->trueIcon('tabler-lock')
 | 
			
		||||
                    ->falseIcon('tabler-lock-open'),
 | 
			
		||||
            ])
 | 
			
		||||
            ->actions([
 | 
			
		||||
                ActionGroup::make([
 | 
			
		||||
@ -84,12 +86,14 @@ class ListBackups extends ListRecords
 | 
			
		||||
                        ->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
 | 
			
		||||
                        ->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
 | 
			
		||||
                        ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup)),
 | 
			
		||||
                        ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
 | 
			
		||||
                        ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
 | 
			
		||||
                    Action::make('download')
 | 
			
		||||
                        ->color('primary')
 | 
			
		||||
                        ->icon('tabler-download')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
 | 
			
		||||
                        ->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true),
 | 
			
		||||
                        ->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
 | 
			
		||||
                        ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
 | 
			
		||||
                    Action::make('restore')
 | 
			
		||||
                        ->color('success')
 | 
			
		||||
                        ->icon('tabler-folder-up')
 | 
			
		||||
@ -138,12 +142,14 @@ class ListBackups extends ListRecords
 | 
			
		||||
                            return Notification::make()
 | 
			
		||||
                                ->title('Restoring Backup')
 | 
			
		||||
                                ->send();
 | 
			
		||||
                        }),
 | 
			
		||||
                        })
 | 
			
		||||
                        ->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
 | 
			
		||||
                    DeleteAction::make('delete')
 | 
			
		||||
                        ->disabled(fn (Backup $backup): bool => $backup->is_locked)
 | 
			
		||||
                        ->disabled(fn (Backup $backup) => $backup->is_locked)
 | 
			
		||||
                        ->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
 | 
			
		||||
                        ->modalSubmitActionLabel('Delete Backup')
 | 
			
		||||
                        ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup)),
 | 
			
		||||
                        ->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup))
 | 
			
		||||
                        ->visible(fn (Backup $backup) => $backup->status !== BackupStatus::InProgress),
 | 
			
		||||
                ]),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
@ -180,7 +186,6 @@ class ListBackups extends ListRecords
 | 
			
		||||
                            ->body($backup->name . ' created.')
 | 
			
		||||
                            ->success()
 | 
			
		||||
                            ->send();
 | 
			
		||||
 | 
			
		||||
                    } catch (HttpException $e) {
 | 
			
		||||
                        return Notification::make()
 | 
			
		||||
                            ->danger()
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Facades\Activity;
 | 
			
		||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
 | 
			
		||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
 | 
			
		||||
use App\Filament\Server\Resources\DatabaseResource;
 | 
			
		||||
@ -82,12 +81,7 @@ class ListDatabases extends ListRecords
 | 
			
		||||
                ViewAction::make()
 | 
			
		||||
                    ->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
 | 
			
		||||
                DeleteAction::make()
 | 
			
		||||
                    ->after(function (Database $database) {
 | 
			
		||||
                        Activity::event('server:database.delete')
 | 
			
		||||
                            ->subject($database)
 | 
			
		||||
                            ->property('name', $database->database)
 | 
			
		||||
                            ->log();
 | 
			
		||||
                    }),
 | 
			
		||||
                    ->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,8 @@ namespace App\Filament\Server\Resources\FileResource\Pages;
 | 
			
		||||
 | 
			
		||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
 | 
			
		||||
use App\Enums\EditorLanguages;
 | 
			
		||||
use App\Exceptions\Http\Server\FileSizeTooLargeException;
 | 
			
		||||
use App\Exceptions\Repository\FileNotEditableException;
 | 
			
		||||
use App\Facades\Activity;
 | 
			
		||||
use App\Filament\Server\Resources\FileResource;
 | 
			
		||||
use App\Livewire\AlertBanner;
 | 
			
		||||
@ -24,6 +26,8 @@ use Filament\Resources\Pages\Page;
 | 
			
		||||
use Filament\Resources\Pages\PageRegistration;
 | 
			
		||||
use Filament\Support\Enums\Alignment;
 | 
			
		||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
use Illuminate\Http\Client\ConnectionException;
 | 
			
		||||
use Illuminate\Routing\Route;
 | 
			
		||||
use Illuminate\Support\Facades\Route as RouteFacade;
 | 
			
		||||
use Livewire\Attributes\Locked;
 | 
			
		||||
@ -45,6 +49,8 @@ class EditFiles extends Page
 | 
			
		||||
    #[Locked]
 | 
			
		||||
    public string $path;
 | 
			
		||||
 | 
			
		||||
    private DaemonFileRepository $fileRepository;
 | 
			
		||||
 | 
			
		||||
    /** @var array<mixed> */
 | 
			
		||||
    public ?array $data = [];
 | 
			
		||||
 | 
			
		||||
@ -66,12 +72,8 @@ class EditFiles extends Page
 | 
			
		||||
                            ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
 | 
			
		||||
                            ->icon('tabler-device-floppy')
 | 
			
		||||
                            ->keyBindings('mod+shift+s')
 | 
			
		||||
                            ->action(function (DaemonFileRepository $fileRepository) use ($server) {
 | 
			
		||||
                                $data = $this->form->getState();
 | 
			
		||||
 | 
			
		||||
                                $fileRepository
 | 
			
		||||
                                    ->setServer($server)
 | 
			
		||||
                                    ->putContent($this->path, $data['editor'] ?? '');
 | 
			
		||||
                            ->action(function () {
 | 
			
		||||
                                $this->getDaemonFileRepository()->putContent($this->path, $this->data['editor'] ?? '');
 | 
			
		||||
 | 
			
		||||
                                Activity::event('server:file.write')
 | 
			
		||||
                                    ->property('file', $this->path)
 | 
			
		||||
@ -90,12 +92,8 @@ class EditFiles extends Page
 | 
			
		||||
                            ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
 | 
			
		||||
                            ->icon('tabler-device-floppy')
 | 
			
		||||
                            ->keyBindings('mod+s')
 | 
			
		||||
                            ->action(function (DaemonFileRepository $fileRepository) use ($server) {
 | 
			
		||||
                                $data = $this->form->getState();
 | 
			
		||||
 | 
			
		||||
                                $fileRepository
 | 
			
		||||
                                    ->setServer($server)
 | 
			
		||||
                                    ->putContent($this->path, $data['editor'] ?? '');
 | 
			
		||||
                            ->action(function () {
 | 
			
		||||
                                $this->getDaemonFileRepository()->putContent($this->path, $this->data['editor'] ?? '');
 | 
			
		||||
 | 
			
		||||
                                Activity::event('server:file.write')
 | 
			
		||||
                                    ->property('file', $this->path)
 | 
			
		||||
@ -117,21 +115,48 @@ class EditFiles extends Page
 | 
			
		||||
                    ->schema([
 | 
			
		||||
                        Select::make('lang')
 | 
			
		||||
                            ->label('Syntax Highlighting')
 | 
			
		||||
                            ->searchable()
 | 
			
		||||
                            ->native(false)
 | 
			
		||||
                            ->live()
 | 
			
		||||
                            ->options(EditorLanguages::class)
 | 
			
		||||
                            ->selectablePlaceholder(false)
 | 
			
		||||
                            ->afterStateUpdated(fn ($state) => $this->dispatch('setLanguage', lang: $state))
 | 
			
		||||
                            ->default(fn () => EditorLanguages::fromWithAlias(pathinfo($this->path, PATHINFO_EXTENSION))),
 | 
			
		||||
                        MonacoEditor::make('editor')
 | 
			
		||||
                            ->label('')
 | 
			
		||||
                            ->placeholderText('')
 | 
			
		||||
                            ->default(function (DaemonFileRepository $fileRepository) use ($server) {
 | 
			
		||||
                            ->hiddenLabel()
 | 
			
		||||
                            ->showPlaceholder(false)
 | 
			
		||||
                            ->default(function () {
 | 
			
		||||
                                try {
 | 
			
		||||
                                    return $fileRepository
 | 
			
		||||
                                        ->setServer($server)
 | 
			
		||||
                                        ->getContent($this->path, config('panel.files.max_edit_size'));
 | 
			
		||||
                                    return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
 | 
			
		||||
                                } catch (FileSizeTooLargeException) {
 | 
			
		||||
                                    AlertBanner::make()
 | 
			
		||||
                                        ->title('<code>' . basename($this->path) . '</code> is too large!')
 | 
			
		||||
                                        ->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
 | 
			
		||||
                                        ->danger()
 | 
			
		||||
                                        ->closable()
 | 
			
		||||
                                        ->send();
 | 
			
		||||
 | 
			
		||||
                                    $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
 | 
			
		||||
                                } catch (FileNotFoundException) {
 | 
			
		||||
                                    abort(404, $this->path . ' not found.');
 | 
			
		||||
                                    AlertBanner::make()
 | 
			
		||||
                                        ->title('<code>' . basename($this->path) . '</code> not found!')
 | 
			
		||||
                                        ->danger()
 | 
			
		||||
                                        ->closable()
 | 
			
		||||
                                        ->send();
 | 
			
		||||
 | 
			
		||||
                                    $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
 | 
			
		||||
                                } catch (FileNotEditableException) {
 | 
			
		||||
                                    AlertBanner::make()
 | 
			
		||||
                                        ->title('<code>' . basename($this->path) . '</code> is a directory')
 | 
			
		||||
                                        ->danger()
 | 
			
		||||
                                        ->closable()
 | 
			
		||||
                                        ->send();
 | 
			
		||||
 | 
			
		||||
                                    $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
 | 
			
		||||
                                } catch (ConnectionException) {
 | 
			
		||||
                                    // Alert banner for this one will be handled by ListFiles
 | 
			
		||||
 | 
			
		||||
                                    $this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
 | 
			
		||||
                                }
 | 
			
		||||
                            })
 | 
			
		||||
                            ->language(fn (Get $get) => $get('lang'))
 | 
			
		||||
@ -149,12 +174,21 @@ class EditFiles extends Page
 | 
			
		||||
        $this->form->fill();
 | 
			
		||||
 | 
			
		||||
        if (str($path)->endsWith('.pelicanignore')) {
 | 
			
		||||
            AlertBanner::make()
 | 
			
		||||
            AlertBanner::make('.pelicanignore_info')
 | 
			
		||||
                ->title('You\'re editing a <code>.pelicanignore</code> file!')
 | 
			
		||||
                ->body('Any files or directories listed in here will be excluded from backups. Wildcards are supported by using an asterisk (<code>*</code>).<br>You can negate a prior rule by prepending an exclamation point (<code>!</code>).')
 | 
			
		||||
                ->info()
 | 
			
		||||
                ->closable()
 | 
			
		||||
                ->send();
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                $this->getDaemonFileRepository()->getDirectory('/');
 | 
			
		||||
            } catch (ConnectionException) {
 | 
			
		||||
                AlertBanner::make('node_connection_error')
 | 
			
		||||
                    ->title('Could not connect to the node!')
 | 
			
		||||
                    ->danger()
 | 
			
		||||
                    ->send();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -200,6 +234,23 @@ class EditFiles extends Page
 | 
			
		||||
        return $breadcrumbs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getDaemonFileRepository(): DaemonFileRepository
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
        $this->fileRepository ??= (new DaemonFileRepository())->setServer($server);
 | 
			
		||||
 | 
			
		||||
        return $this->fileRepository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param  array<string, mixed>  $parameters
 | 
			
		||||
     */
 | 
			
		||||
    public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
 | 
			
		||||
    {
 | 
			
		||||
        return parent::getUrl($parameters, $isAbsolute, $panel, $tenant) . '/';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function route(string $path): PageRegistration
 | 
			
		||||
    {
 | 
			
		||||
        return new PageRegistration(
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,6 @@ use App\Models\Server;
 | 
			
		||||
use App\Repositories\Daemon\DaemonFileRepository;
 | 
			
		||||
use App\Filament\Components\Tables\Columns\BytesColumn;
 | 
			
		||||
use App\Filament\Components\Tables\Columns\DateTimeColumn;
 | 
			
		||||
use App\Livewire\AlertBanner;
 | 
			
		||||
use Filament\Actions\Action as HeaderAction;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Forms\Components\CheckboxList;
 | 
			
		||||
@ -30,16 +29,15 @@ use Filament\Resources\Pages\PageRegistration;
 | 
			
		||||
use Filament\Tables\Actions\Action;
 | 
			
		||||
use Filament\Tables\Actions\ActionGroup;
 | 
			
		||||
use Filament\Tables\Actions\BulkAction;
 | 
			
		||||
use Filament\Tables\Actions\BulkActionGroup;
 | 
			
		||||
use Filament\Tables\Actions\DeleteAction;
 | 
			
		||||
use Filament\Tables\Actions\DeleteBulkAction;
 | 
			
		||||
use Filament\Tables\Actions\EditAction;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Illuminate\Database\Eloquent\Collection;
 | 
			
		||||
use Illuminate\Http\Client\ConnectionException;
 | 
			
		||||
use Illuminate\Http\UploadedFile;
 | 
			
		||||
use Illuminate\Routing\Route;
 | 
			
		||||
use Illuminate\Support\Carbon;
 | 
			
		||||
use Illuminate\Support\Facades\Route as RouteFacade;
 | 
			
		||||
use Livewire\Attributes\Locked;
 | 
			
		||||
 | 
			
		||||
@ -48,25 +46,10 @@ class ListFiles extends ListRecords
 | 
			
		||||
    protected static string $resource = FileResource::class;
 | 
			
		||||
 | 
			
		||||
    #[Locked]
 | 
			
		||||
    public string $path;
 | 
			
		||||
    public string $path = '/';
 | 
			
		||||
 | 
			
		||||
    private DaemonFileRepository $fileRepository;
 | 
			
		||||
 | 
			
		||||
    private bool $isDisabled = false;
 | 
			
		||||
 | 
			
		||||
    public function mount(?string $path = null): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::mount();
 | 
			
		||||
        $this->path = $path ?? '/';
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $this->getDaemonFileRepository()->getDirectory('/');
 | 
			
		||||
        } catch (ConnectionException) {
 | 
			
		||||
            $this->isDisabled = true;
 | 
			
		||||
            $this->getFailureNotification();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getBreadcrumbs(): array
 | 
			
		||||
    {
 | 
			
		||||
        $resource = static::getResource();
 | 
			
		||||
@ -92,8 +75,8 @@ class ListFiles extends ListRecords
 | 
			
		||||
        $files = File::get($server, $this->path);
 | 
			
		||||
 | 
			
		||||
        return $table
 | 
			
		||||
            ->paginated([25, 50, 100, 250])
 | 
			
		||||
            ->defaultPaginationPageOption(50)
 | 
			
		||||
            ->paginated([25, 50])
 | 
			
		||||
            ->defaultPaginationPageOption(25)
 | 
			
		||||
            ->query(fn () => $files->orderByDesc('is_directory'))
 | 
			
		||||
            ->defaultSort('name')
 | 
			
		||||
            ->columns([
 | 
			
		||||
@ -124,21 +107,18 @@ class ListFiles extends ListRecords
 | 
			
		||||
            ->actions([
 | 
			
		||||
                Action::make('view')
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
 | 
			
		||||
                    ->disabled($this->isDisabled)
 | 
			
		||||
                    ->label('Open')
 | 
			
		||||
                    ->icon('tabler-eye')
 | 
			
		||||
                    ->visible(fn (File $file) => $file->is_directory)
 | 
			
		||||
                    ->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
 | 
			
		||||
                EditAction::make('edit')
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
 | 
			
		||||
                    ->disabled($this->isDisabled)
 | 
			
		||||
                    ->icon('tabler-edit')
 | 
			
		||||
                    ->visible(fn (File $file) => $file->canEdit())
 | 
			
		||||
                    ->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
 | 
			
		||||
                ActionGroup::make([
 | 
			
		||||
                    Action::make('rename')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->label('Rename')
 | 
			
		||||
                        ->icon('tabler-forms')
 | 
			
		||||
                        ->form([
 | 
			
		||||
@ -167,7 +147,6 @@ class ListFiles extends ListRecords
 | 
			
		||||
                        }),
 | 
			
		||||
                    Action::make('copy')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->label('Copy')
 | 
			
		||||
                        ->icon('tabler-copy')
 | 
			
		||||
                        ->visible(fn (File $file) => $file->is_file)
 | 
			
		||||
@ -187,14 +166,12 @@ class ListFiles extends ListRecords
 | 
			
		||||
                        }),
 | 
			
		||||
                    Action::make('download')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->label('Download')
 | 
			
		||||
                        ->icon('tabler-download')
 | 
			
		||||
                        ->visible(fn (File $file) => $file->is_file)
 | 
			
		||||
                        ->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
 | 
			
		||||
                    Action::make('move')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->label('Move')
 | 
			
		||||
                        ->icon('tabler-replace')
 | 
			
		||||
                        ->form([
 | 
			
		||||
@ -230,7 +207,6 @@ class ListFiles extends ListRecords
 | 
			
		||||
                        }),
 | 
			
		||||
                    Action::make('permissions')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->label('Permissions')
 | 
			
		||||
                        ->icon('tabler-license')
 | 
			
		||||
                        ->form([
 | 
			
		||||
@ -287,19 +263,26 @@ class ListFiles extends ListRecords
 | 
			
		||||
                        }),
 | 
			
		||||
                    Action::make('archive')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->label('Archive')
 | 
			
		||||
                        ->icon('tabler-archive')
 | 
			
		||||
                        ->action(function (File $file) {
 | 
			
		||||
                            $this->getDaemonFileRepository()->compressFiles($this->path, [$file->name]);
 | 
			
		||||
                        ->form([
 | 
			
		||||
                            TextInput::make('name')
 | 
			
		||||
                                ->label('Archive name')
 | 
			
		||||
                                ->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
 | 
			
		||||
                                ->suffix('.tar.gz'),
 | 
			
		||||
                        ])
 | 
			
		||||
                        ->action(function ($data, File $file) {
 | 
			
		||||
                            $archive = $this->getDaemonFileRepository()->compressFiles($this->path, [$file->name], $data['name']);
 | 
			
		||||
 | 
			
		||||
                            Activity::event('server:file.compress')
 | 
			
		||||
                                ->property('name', $archive['name'])
 | 
			
		||||
                                ->property('directory', $this->path)
 | 
			
		||||
                                ->property('files', [$file->name])
 | 
			
		||||
                                ->log();
 | 
			
		||||
 | 
			
		||||
                            Notification::make()
 | 
			
		||||
                                ->title('Archive created')
 | 
			
		||||
                                ->body($archive['name'])
 | 
			
		||||
                                ->success()
 | 
			
		||||
                                ->send();
 | 
			
		||||
 | 
			
		||||
@ -307,7 +290,6 @@ class ListFiles extends ListRecords
 | 
			
		||||
                        }),
 | 
			
		||||
                    Action::make('unarchive')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->label('Unarchive')
 | 
			
		||||
                        ->icon('tabler-archive')
 | 
			
		||||
                        ->visible(fn (File $file) => $file->isArchive())
 | 
			
		||||
@ -329,7 +311,6 @@ class ListFiles extends ListRecords
 | 
			
		||||
                ]),
 | 
			
		||||
                DeleteAction::make()
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
 | 
			
		||||
                    ->disabled($this->isDisabled)
 | 
			
		||||
                    ->label('')
 | 
			
		||||
                    ->icon('tabler-trash')
 | 
			
		||||
                    ->requiresConfirmation()
 | 
			
		||||
@ -344,75 +325,77 @@ class ListFiles extends ListRecords
 | 
			
		||||
                            ->log();
 | 
			
		||||
                    }),
 | 
			
		||||
            ])
 | 
			
		||||
            ->bulkActions([
 | 
			
		||||
                BulkActionGroup::make([
 | 
			
		||||
                    BulkAction::make('move')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->form([
 | 
			
		||||
                            TextInput::make('location')
 | 
			
		||||
                                ->label('Directory')
 | 
			
		||||
                                ->hint('Enter the new directory, relative to the current directory.')
 | 
			
		||||
                                ->required()
 | 
			
		||||
                                ->live(),
 | 
			
		||||
                            Placeholder::make('new_location')
 | 
			
		||||
                                ->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
 | 
			
		||||
                        ])
 | 
			
		||||
                        ->action(function (Collection $files, $data) {
 | 
			
		||||
                            $location = rtrim($data['location'], '/');
 | 
			
		||||
            ->groupedBulkActions([
 | 
			
		||||
                BulkAction::make('move')
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
 | 
			
		||||
                    ->form([
 | 
			
		||||
                        TextInput::make('location')
 | 
			
		||||
                            ->label('Directory')
 | 
			
		||||
                            ->hint('Enter the new directory, relative to the current directory.')
 | 
			
		||||
                            ->required()
 | 
			
		||||
                            ->live(),
 | 
			
		||||
                        Placeholder::make('new_location')
 | 
			
		||||
                            ->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->action(function (Collection $files, $data) {
 | 
			
		||||
                        $location = rtrim($data['location'], '/');
 | 
			
		||||
 | 
			
		||||
                            $files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
 | 
			
		||||
                            $this->getDaemonFileRepository()
 | 
			
		||||
                                ->renameFiles($this->path, $files);
 | 
			
		||||
                        $files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
 | 
			
		||||
                        $this->getDaemonFileRepository()->renameFiles($this->path, $files);
 | 
			
		||||
 | 
			
		||||
                            Activity::event('server:file.rename')
 | 
			
		||||
                                ->property('directory', $this->path)
 | 
			
		||||
                                ->property('files', $files)
 | 
			
		||||
                                ->log();
 | 
			
		||||
                        Activity::event('server:file.rename')
 | 
			
		||||
                            ->property('directory', $this->path)
 | 
			
		||||
                            ->property('files', $files)
 | 
			
		||||
                            ->log();
 | 
			
		||||
 | 
			
		||||
                            Notification::make()
 | 
			
		||||
                                ->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
 | 
			
		||||
                                ->success()
 | 
			
		||||
                                ->send();
 | 
			
		||||
                        }),
 | 
			
		||||
                    BulkAction::make('archive')
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->action(function (Collection $files) {
 | 
			
		||||
                            $files = $files->map(fn ($file) => $file['name'])->toArray();
 | 
			
		||||
                        Notification::make()
 | 
			
		||||
                            ->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
 | 
			
		||||
                            ->success()
 | 
			
		||||
                            ->send();
 | 
			
		||||
                    }),
 | 
			
		||||
                BulkAction::make('archive')
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
 | 
			
		||||
                    ->form([
 | 
			
		||||
                        TextInput::make('name')
 | 
			
		||||
                            ->label('Archive name')
 | 
			
		||||
                            ->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
 | 
			
		||||
                            ->suffix('.tar.gz'),
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->action(function ($data, Collection $files) {
 | 
			
		||||
                        $files = $files->map(fn ($file) => $file['name'])->toArray();
 | 
			
		||||
 | 
			
		||||
                            $this->getDaemonFileRepository()->compressFiles($this->path, $files);
 | 
			
		||||
                        $archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
 | 
			
		||||
 | 
			
		||||
                            Activity::event('server:file.compress')
 | 
			
		||||
                                ->property('directory', $this->path)
 | 
			
		||||
                                ->property('files', $files)
 | 
			
		||||
                                ->log();
 | 
			
		||||
                        Activity::event('server:file.compress')
 | 
			
		||||
                            ->property('name', $archive['name'])
 | 
			
		||||
                            ->property('directory', $this->path)
 | 
			
		||||
                            ->property('files', $files)
 | 
			
		||||
                            ->log();
 | 
			
		||||
 | 
			
		||||
                            Notification::make()
 | 
			
		||||
                                ->title('Archive created')
 | 
			
		||||
                                ->success()
 | 
			
		||||
                                ->send();
 | 
			
		||||
                        Notification::make()
 | 
			
		||||
                            ->title('Archive created')
 | 
			
		||||
                            ->body($archive['name'])
 | 
			
		||||
                            ->success()
 | 
			
		||||
                            ->send();
 | 
			
		||||
 | 
			
		||||
                            return redirect(ListFiles::getUrl(['path' => $this->path]));
 | 
			
		||||
                        }),
 | 
			
		||||
                    DeleteBulkAction::make()
 | 
			
		||||
                        ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
 | 
			
		||||
                        ->disabled($this->isDisabled)
 | 
			
		||||
                        ->action(function (Collection $files) {
 | 
			
		||||
                            $files = $files->map(fn ($file) => $file['name'])->toArray();
 | 
			
		||||
                            $this->getDaemonFileRepository()->deleteFiles($this->path, $files);
 | 
			
		||||
                        return redirect(ListFiles::getUrl(['path' => $this->path]));
 | 
			
		||||
                    }),
 | 
			
		||||
                DeleteBulkAction::make()
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
 | 
			
		||||
                    ->action(function (Collection $files) {
 | 
			
		||||
                        $files = $files->map(fn ($file) => $file['name'])->toArray();
 | 
			
		||||
                        $this->getDaemonFileRepository()->deleteFiles($this->path, $files);
 | 
			
		||||
 | 
			
		||||
                            Activity::event('server:file.delete')
 | 
			
		||||
                                ->property('directory', $this->path)
 | 
			
		||||
                                ->property('files', $files)
 | 
			
		||||
                                ->log();
 | 
			
		||||
                        Activity::event('server:file.delete')
 | 
			
		||||
                            ->property('directory', $this->path)
 | 
			
		||||
                            ->property('files', $files)
 | 
			
		||||
                            ->log();
 | 
			
		||||
 | 
			
		||||
                            Notification::make()
 | 
			
		||||
                                ->title(count($files) . ' Files deleted.')
 | 
			
		||||
                                ->success()
 | 
			
		||||
                                ->send();
 | 
			
		||||
                        }),
 | 
			
		||||
                ]),
 | 
			
		||||
                        Notification::make()
 | 
			
		||||
                            ->title(count($files) . ' Files deleted.')
 | 
			
		||||
                            ->success()
 | 
			
		||||
                            ->send();
 | 
			
		||||
                    }),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -424,7 +407,6 @@ class ListFiles extends ListRecords
 | 
			
		||||
        return [
 | 
			
		||||
            HeaderAction::make('new_file')
 | 
			
		||||
                ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
 | 
			
		||||
                ->disabled($this->isDisabled)
 | 
			
		||||
                ->label('New File')
 | 
			
		||||
                ->color('gray')
 | 
			
		||||
                ->keyBindings('')
 | 
			
		||||
@ -442,6 +424,8 @@ class ListFiles extends ListRecords
 | 
			
		||||
                        ->required(),
 | 
			
		||||
                    Select::make('lang')
 | 
			
		||||
                        ->label('Syntax Highlighting')
 | 
			
		||||
                        ->searchable()
 | 
			
		||||
                        ->native(false)
 | 
			
		||||
                        ->live()
 | 
			
		||||
                        ->options(EditorLanguages::class)
 | 
			
		||||
                        ->selectablePlaceholder(false)
 | 
			
		||||
@ -454,7 +438,6 @@ class ListFiles extends ListRecords
 | 
			
		||||
                ]),
 | 
			
		||||
            HeaderAction::make('new_folder')
 | 
			
		||||
                ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
 | 
			
		||||
                ->disabled($this->isDisabled)
 | 
			
		||||
                ->label('New Folder')
 | 
			
		||||
                ->color('gray')
 | 
			
		||||
                ->action(function ($data) {
 | 
			
		||||
@ -471,7 +454,6 @@ class ListFiles extends ListRecords
 | 
			
		||||
                ]),
 | 
			
		||||
            HeaderAction::make('upload')
 | 
			
		||||
                ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
 | 
			
		||||
                ->disabled($this->isDisabled)
 | 
			
		||||
                ->label('Upload')
 | 
			
		||||
                ->action(function ($data) {
 | 
			
		||||
                    if (count($data['files']) > 0 && !isset($data['url'])) {
 | 
			
		||||
@ -521,14 +503,14 @@ class ListFiles extends ListRecords
 | 
			
		||||
                ]),
 | 
			
		||||
            HeaderAction::make('search')
 | 
			
		||||
                ->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
 | 
			
		||||
                ->disabled($this->isDisabled)
 | 
			
		||||
                ->label('Global Search')
 | 
			
		||||
                ->modalSubmitActionLabel('Search')
 | 
			
		||||
                ->form([
 | 
			
		||||
                    TextInput::make('searchTerm')
 | 
			
		||||
                        ->placeholder('Enter a search term, e.g. *.txt')
 | 
			
		||||
                        ->required()
 | 
			
		||||
                        ->regex('/^[^*]*\*?[^*]*$/')
 | 
			
		||||
                        ->minLength(3),
 | 
			
		||||
                        ->minValue(3),
 | 
			
		||||
                ])
 | 
			
		||||
                ->action(fn ($data) => redirect(SearchFiles::getUrl([
 | 
			
		||||
                    'searchTerm' => $data['searchTerm'],
 | 
			
		||||
@ -563,14 +545,6 @@ class ListFiles extends ListRecords
 | 
			
		||||
        return $this->fileRepository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getFailureNotification(): AlertBanner
 | 
			
		||||
    {
 | 
			
		||||
        return AlertBanner::make()
 | 
			
		||||
            ->title('Could not connect to the node!')
 | 
			
		||||
            ->danger()
 | 
			
		||||
            ->send();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function route(string $path): PageRegistration
 | 
			
		||||
    {
 | 
			
		||||
        return new PageRegistration(
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ use Filament\Resources\Pages\ListRecords;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
use Filament\Tables\Table;
 | 
			
		||||
use Livewire\Attributes\Locked;
 | 
			
		||||
use Livewire\Attributes\Url;
 | 
			
		||||
 | 
			
		||||
class SearchFiles extends ListRecords
 | 
			
		||||
{
 | 
			
		||||
@ -22,15 +23,8 @@ class SearchFiles extends ListRecords
 | 
			
		||||
    #[Locked]
 | 
			
		||||
    public string $searchTerm;
 | 
			
		||||
 | 
			
		||||
    #[Locked]
 | 
			
		||||
    public string $path;
 | 
			
		||||
 | 
			
		||||
    public function mount(?string $searchTerm = null, ?string $path = null): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::mount();
 | 
			
		||||
        $this->searchTerm = $searchTerm;
 | 
			
		||||
        $this->path = $path ?? '/';
 | 
			
		||||
    }
 | 
			
		||||
    #[Url]
 | 
			
		||||
    public string $path = '/';
 | 
			
		||||
 | 
			
		||||
    public function getBreadcrumbs(): array
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -4,9 +4,12 @@ namespace App\Filament\Server\Resources;
 | 
			
		||||
 | 
			
		||||
use App\Filament\Server\Resources\ScheduleResource\Pages;
 | 
			
		||||
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
 | 
			
		||||
use App\Helpers\Utilities;
 | 
			
		||||
use App\Models\Permission;
 | 
			
		||||
use App\Models\Schedule;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Forms\Components\Actions;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action;
 | 
			
		||||
@ -17,7 +20,9 @@ use Filament\Forms\Components\Toggle;
 | 
			
		||||
use Filament\Forms\Components\ToggleButtons;
 | 
			
		||||
use Filament\Forms\Form;
 | 
			
		||||
use Filament\Forms\Set;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Filament\Resources\Resource;
 | 
			
		||||
use Filament\Support\Exceptions\Halt;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
 | 
			
		||||
class ScheduleResource extends Resource
 | 
			
		||||
@ -314,4 +319,18 @@ class ScheduleResource extends Resource
 | 
			
		||||
            'edit' => Pages\EditSchedule::route('/{record}/edit'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getNextRun(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek);
 | 
			
		||||
        } catch (Exception) {
 | 
			
		||||
            Notification::make()
 | 
			
		||||
                ->title('The cron data provided does not evaluate to a valid expression')
 | 
			
		||||
                ->danger()
 | 
			
		||||
                ->send();
 | 
			
		||||
 | 
			
		||||
            throw new Halt();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,14 +2,10 @@
 | 
			
		||||
 | 
			
		||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
 | 
			
		||||
 | 
			
		||||
use App\Exceptions\DisplayException;
 | 
			
		||||
use App\Facades\Activity;
 | 
			
		||||
use App\Filament\Server\Resources\ScheduleResource;
 | 
			
		||||
use App\Helpers\Utilities;
 | 
			
		||||
use App\Models\Schedule;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Resources\Pages\CreateRecord;
 | 
			
		||||
 | 
			
		||||
@ -39,27 +35,18 @@ class CreateSchedule extends CreateRecord
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($data['next_run_at'])) {
 | 
			
		||||
            $data['next_run_at'] = $this->getNextRunAt($data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], $data['cron_day_of_week']);
 | 
			
		||||
            $data['next_run_at'] = ScheduleResource::getNextRun(
 | 
			
		||||
                $data['cron_minute'],
 | 
			
		||||
                $data['cron_hour'],
 | 
			
		||||
                $data['cron_day_of_month'],
 | 
			
		||||
                $data['cron_month'],
 | 
			
		||||
                $data['cron_day_of_week']
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getNextRunAt(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            return Utilities::getScheduleNextRunDate(
 | 
			
		||||
                $minute,
 | 
			
		||||
                $hour,
 | 
			
		||||
                $dayOfMonth,
 | 
			
		||||
                $month,
 | 
			
		||||
                $dayOfWeek
 | 
			
		||||
            );
 | 
			
		||||
        } catch (Exception) {
 | 
			
		||||
            throw new DisplayException('The cron data provided does not evaluate to a valid expression.');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getBreadcrumbs(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [];
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,19 @@ class EditSchedule extends EditRecord
 | 
			
		||||
            ->log();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function mutateFormDataBeforeSave(array $data): array
 | 
			
		||||
    {
 | 
			
		||||
        $data['next_run_at'] = ScheduleResource::getNextRun(
 | 
			
		||||
            $data['cron_minute'],
 | 
			
		||||
            $data['cron_hour'],
 | 
			
		||||
            $data['cron_day_of_month'],
 | 
			
		||||
            $data['cron_month'],
 | 
			
		||||
            $data['cron_day_of_week']
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return $data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getHeaderActions(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
 | 
			
		||||
@ -39,8 +39,10 @@ class ListSchedules extends ListRecords
 | 
			
		||||
                    ->sortable(),
 | 
			
		||||
                DateTimeColumn::make('next_run_at')
 | 
			
		||||
                    ->label('Next run')
 | 
			
		||||
                    ->placeholder('Never')
 | 
			
		||||
                    ->since()
 | 
			
		||||
                    ->sortable(),
 | 
			
		||||
                    ->sortable()
 | 
			
		||||
                    ->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
 | 
			
		||||
            ])
 | 
			
		||||
            ->actions([
 | 
			
		||||
                ViewAction::make(),
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ namespace App\Filament\Server\Resources;
 | 
			
		||||
use App\Filament\Server\Resources\UserResource\Pages;
 | 
			
		||||
use App\Models\Permission;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Models\Subuser;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use App\Services\Subusers\SubuserDeletionService;
 | 
			
		||||
use App\Services\Subusers\SubuserUpdateService;
 | 
			
		||||
@ -20,8 +19,8 @@ use Filament\Forms\Components\Tabs\Tab;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Set;
 | 
			
		||||
use Filament\Notifications\Notification;
 | 
			
		||||
use Filament\Tables\Actions\DeleteAction;
 | 
			
		||||
use Filament\Resources\Resource;
 | 
			
		||||
use Filament\Tables\Actions\DeleteAction;
 | 
			
		||||
use Filament\Tables\Actions\EditAction;
 | 
			
		||||
use Filament\Tables\Columns\ImageColumn;
 | 
			
		||||
use Filament\Tables\Columns\TextColumn;
 | 
			
		||||
@ -84,6 +83,35 @@ class UserResource extends Resource
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        $tabs = [];
 | 
			
		||||
        $permissionsArray = [];
 | 
			
		||||
 | 
			
		||||
        foreach (Permission::permissionData() as $data) {
 | 
			
		||||
            $options = [];
 | 
			
		||||
            $descriptions = [];
 | 
			
		||||
 | 
			
		||||
            foreach ($data['permissions'] as $permission) {
 | 
			
		||||
                $options[$permission] = str($permission)->headline();
 | 
			
		||||
                $descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
 | 
			
		||||
                $permissionsArray[$data['name']][] = $permission;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $tabs[] = Tab::make(str($data['name'])->headline())
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Section::make()
 | 
			
		||||
                        ->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
 | 
			
		||||
                        ->icon($data['icon'])
 | 
			
		||||
                        ->schema([
 | 
			
		||||
                            CheckboxList::make($data['name'])
 | 
			
		||||
                                ->label('')
 | 
			
		||||
                                ->bulkToggleable()
 | 
			
		||||
                                ->columns(2)
 | 
			
		||||
                                ->options($options)
 | 
			
		||||
                                ->descriptions($descriptions),
 | 
			
		||||
                        ]),
 | 
			
		||||
                ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $table
 | 
			
		||||
            ->paginated(false)
 | 
			
		||||
            ->searchable(false)
 | 
			
		||||
@ -91,21 +119,21 @@ class UserResource extends Resource
 | 
			
		||||
                ImageColumn::make('picture')
 | 
			
		||||
                    ->visibleFrom('lg')
 | 
			
		||||
                    ->label('')
 | 
			
		||||
                    ->extraImgAttributes(['class' => 'rounded-full'])
 | 
			
		||||
                    ->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
 | 
			
		||||
                    ->alignCenter()->circular()
 | 
			
		||||
                    ->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)),
 | 
			
		||||
                TextColumn::make('username')
 | 
			
		||||
                    ->searchable(),
 | 
			
		||||
                TextColumn::make('email')
 | 
			
		||||
                    ->searchable(),
 | 
			
		||||
                TextColumn::make('permissions')
 | 
			
		||||
                    ->state(fn (User $user) => count(Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first()->permissions)),
 | 
			
		||||
                    ->state(fn (User $user) => count($server->subusers->where('user_id', $user->id)->first()->permissions)),
 | 
			
		||||
            ])
 | 
			
		||||
            ->actions([
 | 
			
		||||
                DeleteAction::make()
 | 
			
		||||
                    ->label('Remove User')
 | 
			
		||||
                    ->hidden(fn (User $user) => auth()->user()->id === $user->id)
 | 
			
		||||
                    ->action(function (User $user, SubuserDeletionService $subuserDeletionService) use ($server) {
 | 
			
		||||
                        $subuser = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first();
 | 
			
		||||
                        $subuser = $server->subusers->where('user_id', $user->id)->first();
 | 
			
		||||
                        $subuserDeletionService->handle($subuser, $server);
 | 
			
		||||
 | 
			
		||||
                        Notification::make()
 | 
			
		||||
@ -119,7 +147,7 @@ class UserResource extends Resource
 | 
			
		||||
                    ->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_UPDATE, $server))
 | 
			
		||||
                    ->modalHeading(fn (User $user) => 'Editing ' . $user->email)
 | 
			
		||||
                    ->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) {
 | 
			
		||||
                        $subuser = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first();
 | 
			
		||||
                        $subuser = $server->subusers->where('user_id', $user->id)->first();
 | 
			
		||||
 | 
			
		||||
                        $permissions = collect($data)
 | 
			
		||||
                            ->forget('email')
 | 
			
		||||
@ -159,67 +187,8 @@ class UserResource extends Resource
 | 
			
		||||
                                Actions::make([
 | 
			
		||||
                                    Action::make('assignAll')
 | 
			
		||||
                                        ->label('Assign All')
 | 
			
		||||
                                        ->action(function (Set $set) {
 | 
			
		||||
                                            $permissions = [
 | 
			
		||||
                                                'control' => [
 | 
			
		||||
                                                    'console',
 | 
			
		||||
                                                    'start',
 | 
			
		||||
                                                    'stop',
 | 
			
		||||
                                                    'restart',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                                'user' => [
 | 
			
		||||
                                                    'read',
 | 
			
		||||
                                                    'create',
 | 
			
		||||
                                                    'update',
 | 
			
		||||
                                                    'delete',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                                'file' => [
 | 
			
		||||
                                                    'read',
 | 
			
		||||
                                                    'read-content',
 | 
			
		||||
                                                    'create',
 | 
			
		||||
                                                    'update',
 | 
			
		||||
                                                    'delete',
 | 
			
		||||
                                                    'archive',
 | 
			
		||||
                                                    'sftp',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                                'backup' => [
 | 
			
		||||
                                                    'read',
 | 
			
		||||
                                                    'create',
 | 
			
		||||
                                                    'delete',
 | 
			
		||||
                                                    'download',
 | 
			
		||||
                                                    'restore',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                                'allocation' => [
 | 
			
		||||
                                                    'read',
 | 
			
		||||
                                                    'create',
 | 
			
		||||
                                                    'update',
 | 
			
		||||
                                                    'delete',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                                'startup' => [
 | 
			
		||||
                                                    'read',
 | 
			
		||||
                                                    'update',
 | 
			
		||||
                                                    'docker-image',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                                'database' => [
 | 
			
		||||
                                                    'read',
 | 
			
		||||
                                                    'create',
 | 
			
		||||
                                                    'update',
 | 
			
		||||
                                                    'delete',
 | 
			
		||||
                                                    'view_password',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                                'schedule' => [
 | 
			
		||||
                                                    'read',
 | 
			
		||||
                                                    'create',
 | 
			
		||||
                                                    'update',
 | 
			
		||||
                                                    'delete',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                                'settings' => [
 | 
			
		||||
                                                    'rename',
 | 
			
		||||
                                                    'reinstall',
 | 
			
		||||
                                                    'activity',
 | 
			
		||||
                                                ],
 | 
			
		||||
                                            ];
 | 
			
		||||
 | 
			
		||||
                                        ->action(function (Set $set) use ($permissionsArray) {
 | 
			
		||||
                                            $permissions = $permissionsArray;
 | 
			
		||||
                                            foreach ($permissions as $key => $value) {
 | 
			
		||||
                                                $allValues = array_unique($value);
 | 
			
		||||
                                                $set($key, $allValues);
 | 
			
		||||
@ -234,243 +203,25 @@ class UserResource extends Resource
 | 
			
		||||
                                    ]),
 | 
			
		||||
                                Tabs::make()
 | 
			
		||||
                                    ->columnSpanFull()
 | 
			
		||||
                                    ->schema([
 | 
			
		||||
                                        Tab::make('Console')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.control_desc'))
 | 
			
		||||
                                                    ->icon('tabler-terminal-2')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('control')
 | 
			
		||||
                                                            ->formatStateUsing(function (User $user, Set $set) use ($server) {
 | 
			
		||||
                                                                $permissionsArray = Subuser::query()
 | 
			
		||||
                                                                    ->where('user_id', $user->id)
 | 
			
		||||
                                                                    ->where('server_id', $server->id)
 | 
			
		||||
                                                                    ->first()
 | 
			
		||||
                                                                    ->permissions;
 | 
			
		||||
 | 
			
		||||
                                                                $transformedPermissions = [];
 | 
			
		||||
 | 
			
		||||
                                                                foreach ($permissionsArray as $permission) {
 | 
			
		||||
                                                                    [$group, $action] = explode('.', $permission, 2);
 | 
			
		||||
                                                                    $transformedPermissions[$group][] = $action;
 | 
			
		||||
                                                                }
 | 
			
		||||
 | 
			
		||||
                                                                foreach ($transformedPermissions as $key => $value) {
 | 
			
		||||
                                                                    $set($key, $value);
 | 
			
		||||
                                                                }
 | 
			
		||||
 | 
			
		||||
                                                                return $transformedPermissions['control'] ?? [];
 | 
			
		||||
                                                            })
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'console' => 'Console',
 | 
			
		||||
                                                                'start' => 'Start',
 | 
			
		||||
                                                                'stop' => 'Stop',
 | 
			
		||||
                                                                'restart' => 'Restart',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'console' => trans('server/users.permissions.control_console'),
 | 
			
		||||
                                                                'start' => trans('server/users.permissions.control_start'),
 | 
			
		||||
                                                                'stop' => trans('server/users.permissions.control_stop'),
 | 
			
		||||
                                                                'restart' => trans('server/users.permissions.control_restart'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Tab::make('User')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.user_desc'))
 | 
			
		||||
                                                    ->icon('tabler-users')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('user')
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'read' => 'Read',
 | 
			
		||||
                                                                'create' => 'Create',
 | 
			
		||||
                                                                'update' => 'Update',
 | 
			
		||||
                                                                'delete' => 'Delete',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'create' => trans('server/users.permissions.user_create'),
 | 
			
		||||
                                                                'read' => trans('server/users.permissions.user_read'),
 | 
			
		||||
                                                                'update' => trans('server/users.permissions.user_update'),
 | 
			
		||||
                                                                'delete' => trans('server/users.permissions.user_delete'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Tab::make('File')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.file_desc'))
 | 
			
		||||
                                                    ->icon('tabler-folders')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('file')
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'read' => 'Read',
 | 
			
		||||
                                                                'read-content' => 'Read Content',
 | 
			
		||||
                                                                'create' => 'Create',
 | 
			
		||||
                                                                'update' => 'Update',
 | 
			
		||||
                                                                'delete' => 'Delete',
 | 
			
		||||
                                                                'archive' => 'Archive',
 | 
			
		||||
                                                                'sftp' => 'SFTP',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'create' => trans('server/users.permissions.file_create'),
 | 
			
		||||
                                                                'read' => trans('server/users.permissions.file_read'),
 | 
			
		||||
                                                                'read-content' => trans('server/users.permissions.file_read_content'),
 | 
			
		||||
                                                                'update' => trans('server/users.permissions.file_update'),
 | 
			
		||||
                                                                'delete' => trans('server/users.permissions.file_delete'),
 | 
			
		||||
                                                                'archive' => trans('server/users.permissions.file_archive'),
 | 
			
		||||
                                                                'sftp' => trans('server/users.permissions.file_sftp'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Tab::make('Backup')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.backup_desc'))
 | 
			
		||||
                                                    ->icon('tabler-download')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('backup')
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'read' => 'Read',
 | 
			
		||||
                                                                'create' => 'Create',
 | 
			
		||||
                                                                'delete' => 'Delete',
 | 
			
		||||
                                                                'download' => 'Download',
 | 
			
		||||
                                                                'restore' => 'Restore',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'create' => trans('server/users.permissions.backup_create'),
 | 
			
		||||
                                                                'read' => trans('server/users.permissions.backup_read'),
 | 
			
		||||
                                                                'delete' => trans('server/users.permissions.backup_delete'),
 | 
			
		||||
                                                                'download' => trans('server/users.permissions.backup_download'),
 | 
			
		||||
                                                                'restore' => trans('server/users.permissions.backup_restore'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Tab::make('Allocation')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.allocation_desc'))
 | 
			
		||||
                                                    ->icon('tabler-network')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('allocation')
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'read' => 'Read',
 | 
			
		||||
                                                                'create' => 'Create',
 | 
			
		||||
                                                                'update' => 'Update',
 | 
			
		||||
                                                                'delete' => 'Delete',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'read' => trans('server/users.permissions.allocation_read'),
 | 
			
		||||
                                                                'create' => trans('server/users.permissions.allocation_create'),
 | 
			
		||||
                                                                'update' => trans('server/users.permissions.allocation_update'),
 | 
			
		||||
                                                                'delete' => trans('server/users.permissions.allocation_delete'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Tab::make('Startup')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.startup_desc'))
 | 
			
		||||
                                                    ->icon('tabler-question-mark')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('startup')
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'read' => 'Read',
 | 
			
		||||
                                                                'update' => 'Update',
 | 
			
		||||
                                                                'docker-image' => 'Docker Image',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'read' => trans('server/users.permissions.startup_read'),
 | 
			
		||||
                                                                'update' => trans('server/users.permissions.startup_update'),
 | 
			
		||||
                                                                'docker-image' => trans('server/users.permissions.startup_docker_image'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Tab::make('Database')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.database_desc'))
 | 
			
		||||
                                                    ->icon('tabler-database')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('database')
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'read' => 'Read',
 | 
			
		||||
                                                                'create' => 'Create',
 | 
			
		||||
                                                                'update' => 'Update',
 | 
			
		||||
                                                                'delete' => 'Delete',
 | 
			
		||||
                                                                'view_password' => 'View Password',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'read' => trans('server/users.permissions.database_read'),
 | 
			
		||||
                                                                'create' => trans('server/users.permissions.database_create'),
 | 
			
		||||
                                                                'update' => trans('server/users.permissions.database_update'),
 | 
			
		||||
                                                                'delete' => trans('server/users.permissions.database_delete'),
 | 
			
		||||
                                                                'view_password' => trans('server/users.permissions.database_view_password'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Tab::make('Schedule')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.schedule_desc'))
 | 
			
		||||
                                                    ->icon('tabler-clock')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('schedule')
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'read' => 'Read',
 | 
			
		||||
                                                                'create' => 'Create',
 | 
			
		||||
                                                                'update' => 'Update',
 | 
			
		||||
                                                                'delete' => 'Delete',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'read' => trans('server/users.permissions.schedule_read'),
 | 
			
		||||
                                                                'create' => trans('server/users.permissions.schedule_create'),
 | 
			
		||||
                                                                'update' => trans('server/users.permissions.schedule_update'),
 | 
			
		||||
                                                                'delete' => trans('server/users.permissions.schedule_delete'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                        Tab::make('Settings')
 | 
			
		||||
                                            ->schema([
 | 
			
		||||
                                                Section::make()
 | 
			
		||||
                                                    ->description(trans('server/users.permissions.settings_desc'))
 | 
			
		||||
                                                    ->icon('tabler-settings')
 | 
			
		||||
                                                    ->schema([
 | 
			
		||||
                                                        CheckboxList::make('settings')
 | 
			
		||||
                                                            ->bulkToggleable()
 | 
			
		||||
                                                            ->label('')
 | 
			
		||||
                                                            ->options([
 | 
			
		||||
                                                                'rename' => 'Rename',
 | 
			
		||||
                                                                'reinstall' => 'Reinstall',
 | 
			
		||||
                                                                'activity' => 'Activity',
 | 
			
		||||
                                                            ])
 | 
			
		||||
                                                            ->descriptions([
 | 
			
		||||
                                                                'rename' => trans('server/users.permissions.setting_rename'),
 | 
			
		||||
                                                                'reinstall' => trans('server/users.permissions.setting_reinstall'),
 | 
			
		||||
                                                                'activity' => trans('server/users.permissions.activity_desc'),
 | 
			
		||||
                                                            ]),
 | 
			
		||||
                                                    ]),
 | 
			
		||||
                                            ]),
 | 
			
		||||
                                    ]),
 | 
			
		||||
                                    ->schema($tabs),
 | 
			
		||||
                            ]),
 | 
			
		||||
                    ]),
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->mutateRecordDataUsing(function ($data, User $user) use ($server) {
 | 
			
		||||
                        $permissionsArray = $server->subusers->where('user_id', $user->id)->first()->permissions;
 | 
			
		||||
 | 
			
		||||
                        $transformedPermissions = [];
 | 
			
		||||
 | 
			
		||||
                        foreach ($permissionsArray as $permission) {
 | 
			
		||||
                            [$group, $action] = explode('.', $permission, 2);
 | 
			
		||||
                            $transformedPermissions[$group][] = $action;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        foreach ($transformedPermissions as $key => $value) {
 | 
			
		||||
                            $data[$key] = $value;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return $data;
 | 
			
		||||
                    }),
 | 
			
		||||
            ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,12 +10,13 @@ use App\Services\Subusers\SubuserCreationService;
 | 
			
		||||
use Exception;
 | 
			
		||||
use Filament\Actions;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Forms\Components\Actions as assignAll;
 | 
			
		||||
use Filament\Forms\Components\Actions\Action;
 | 
			
		||||
use Filament\Forms\Components\Actions as assignAll;
 | 
			
		||||
use Filament\Forms\Components\CheckboxList;
 | 
			
		||||
use Filament\Forms\Components\Grid;
 | 
			
		||||
use Filament\Forms\Components\Section;
 | 
			
		||||
use Filament\Forms\Components\Tabs;
 | 
			
		||||
use Filament\Forms\Components\Tabs\Tab;
 | 
			
		||||
use Filament\Forms\Components\TextInput;
 | 
			
		||||
use Filament\Forms\Get;
 | 
			
		||||
use Filament\Forms\Set;
 | 
			
		||||
@ -31,6 +32,35 @@ class ListUsers extends ListRecords
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        $tabs = [];
 | 
			
		||||
        $permissionsArray = [];
 | 
			
		||||
 | 
			
		||||
        foreach (Permission::permissionData() as $data) {
 | 
			
		||||
            $options = [];
 | 
			
		||||
            $descriptions = [];
 | 
			
		||||
 | 
			
		||||
            foreach ($data['permissions'] as $permission) {
 | 
			
		||||
                $options[$permission] = str($permission)->headline();
 | 
			
		||||
                $descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
 | 
			
		||||
                $permissionsArray[$data['name']][] = $permission;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $tabs[] = Tab::make(str($data['name'])->headline())
 | 
			
		||||
                ->schema([
 | 
			
		||||
                    Section::make()
 | 
			
		||||
                        ->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
 | 
			
		||||
                        ->icon($data['icon'])
 | 
			
		||||
                        ->schema([
 | 
			
		||||
                            CheckboxList::make($data['name'])
 | 
			
		||||
                                ->label('')
 | 
			
		||||
                                ->bulkToggleable()
 | 
			
		||||
                                ->columns(2)
 | 
			
		||||
                                ->options($options)
 | 
			
		||||
                                ->descriptions($descriptions),
 | 
			
		||||
                        ]),
 | 
			
		||||
                ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            Actions\CreateAction::make('invite')
 | 
			
		||||
                ->label('Invite User')
 | 
			
		||||
@ -59,72 +89,10 @@ class ListUsers extends ListRecords
 | 
			
		||||
                            assignAll::make([
 | 
			
		||||
                                Action::make('assignAll')
 | 
			
		||||
                                    ->label('Assign All')
 | 
			
		||||
                                    ->action(function (Set $set, Get $get) {
 | 
			
		||||
                                        $permissions = [
 | 
			
		||||
                                            'control' => [
 | 
			
		||||
                                                'console',
 | 
			
		||||
                                                'start',
 | 
			
		||||
                                                'stop',
 | 
			
		||||
                                                'restart',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'user' => [
 | 
			
		||||
                                                'read',
 | 
			
		||||
                                                'create',
 | 
			
		||||
                                                'update',
 | 
			
		||||
                                                'delete',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'file' => [
 | 
			
		||||
                                                'read',
 | 
			
		||||
                                                'read-content',
 | 
			
		||||
                                                'create',
 | 
			
		||||
                                                'update',
 | 
			
		||||
                                                'delete',
 | 
			
		||||
                                                'archive',
 | 
			
		||||
                                                'sftp',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'backup' => [
 | 
			
		||||
                                                'read',
 | 
			
		||||
                                                'create',
 | 
			
		||||
                                                'delete',
 | 
			
		||||
                                                'download',
 | 
			
		||||
                                                'restore',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'allocation' => [
 | 
			
		||||
                                                'read',
 | 
			
		||||
                                                'create',
 | 
			
		||||
                                                'update',
 | 
			
		||||
                                                'delete',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'startup' => [
 | 
			
		||||
                                                'read',
 | 
			
		||||
                                                'update',
 | 
			
		||||
                                                'docker-image',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'database' => [
 | 
			
		||||
                                                'read',
 | 
			
		||||
                                                'create',
 | 
			
		||||
                                                'update',
 | 
			
		||||
                                                'delete',
 | 
			
		||||
                                                'view_password',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'schedule' => [
 | 
			
		||||
                                                'read',
 | 
			
		||||
                                                'create',
 | 
			
		||||
                                                'update',
 | 
			
		||||
                                                'delete',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'settings' => [
 | 
			
		||||
                                                'rename',
 | 
			
		||||
                                                'reinstall',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                            'activity' => [
 | 
			
		||||
                                                'read',
 | 
			
		||||
                                            ],
 | 
			
		||||
                                        ];
 | 
			
		||||
 | 
			
		||||
                                    ->action(function (Set $set, Get $get) use ($permissionsArray) {
 | 
			
		||||
                                        $permissions = $permissionsArray;
 | 
			
		||||
                                        foreach ($permissions as $key => $value) {
 | 
			
		||||
                                            $currentValues = $get($key) ?? [];
 | 
			
		||||
                                            $allValues = array_unique(array_merge($currentValues, $value));
 | 
			
		||||
                                            $allValues = array_unique($value);
 | 
			
		||||
                                            $set($key, $allValues);
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }),
 | 
			
		||||
@ -137,249 +105,7 @@ class ListUsers extends ListRecords
 | 
			
		||||
                                ]),
 | 
			
		||||
                            Tabs::make()
 | 
			
		||||
                                ->columnSpanFull()
 | 
			
		||||
                                ->schema([
 | 
			
		||||
                                    Tabs\Tab::make('Console')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.control_desc'))
 | 
			
		||||
                                                ->icon('tabler-terminal-2')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('control')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'console' => 'Console',
 | 
			
		||||
                                                            'start' => 'Start',
 | 
			
		||||
                                                            'stop' => 'Stop',
 | 
			
		||||
                                                            'restart' => 'Restart',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'console' => trans('server/users.permissions.control_console'),
 | 
			
		||||
                                                            'start' => trans('server/users.permissions.control_start'),
 | 
			
		||||
                                                            'stop' => trans('server/users.permissions.control_stop'),
 | 
			
		||||
                                                            'restart' => trans('server/users.permissions.control_restart'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('User')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.user_desc'))
 | 
			
		||||
                                                ->icon('tabler-users')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('user')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'read' => 'Read',
 | 
			
		||||
                                                            'create' => 'Create',
 | 
			
		||||
                                                            'update' => 'Update',
 | 
			
		||||
                                                            'delete' => 'Delete',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'create' => trans('server/users.permissions.user_create'),
 | 
			
		||||
                                                            'read' => trans('server/users.permissions.user_read'),
 | 
			
		||||
                                                            'update' => trans('server/users.permissions.user_update'),
 | 
			
		||||
                                                            'delete' => trans('server/users.permissions.user_delete'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('File')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.file_desc'))
 | 
			
		||||
                                                ->icon('tabler-folders')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('file')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'read' => 'Read',
 | 
			
		||||
                                                            'read-content' => 'Read Content',
 | 
			
		||||
                                                            'create' => 'Create',
 | 
			
		||||
                                                            'update' => 'Update',
 | 
			
		||||
                                                            'delete' => 'Delete',
 | 
			
		||||
                                                            'archive' => 'Archive',
 | 
			
		||||
                                                            'sftp' => 'SFTP',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'create' => trans('server/users.permissions.file_create'),
 | 
			
		||||
                                                            'read' => trans('server/users.permissions.file_read'),
 | 
			
		||||
                                                            'read-content' => trans('server/users.permissions.file_read_content'),
 | 
			
		||||
                                                            'update' => trans('server/users.permissions.file_update'),
 | 
			
		||||
                                                            'delete' => trans('server/users.permissions.file_delete'),
 | 
			
		||||
                                                            'archive' => trans('server/users.permissions.file_archive'),
 | 
			
		||||
                                                            'sftp' => trans('server/users.permissions.file_sftp'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('Backup')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.backup_desc'))
 | 
			
		||||
                                                ->icon('tabler-download')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('backup')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'read' => 'Read',
 | 
			
		||||
                                                            'create' => 'Create',
 | 
			
		||||
                                                            'delete' => 'Delete',
 | 
			
		||||
                                                            'download' => 'Download',
 | 
			
		||||
                                                            'restore' => 'Restore',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'create' => trans('server/users.permissions.backup_create'),
 | 
			
		||||
                                                            'read' => trans('server/users.permissions.backup_read'),
 | 
			
		||||
                                                            'delete' => trans('server/users.permissions.backup_delete'),
 | 
			
		||||
                                                            'download' => trans('server/users.permissions.backup_download'),
 | 
			
		||||
                                                            'restore' => trans('server/users.permissions.backup_restore'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('Allocation')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.allocation_desc'))
 | 
			
		||||
                                                ->icon('tabler-network')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('allocation')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'read' => 'Read',
 | 
			
		||||
                                                            'create' => 'Create',
 | 
			
		||||
                                                            'update' => 'Update',
 | 
			
		||||
                                                            'delete' => 'Delete',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'read' => trans('server/users.permissions.allocation_read'),
 | 
			
		||||
                                                            'create' => trans('server/users.permissions.allocation_create'),
 | 
			
		||||
                                                            'update' => trans('server/users.permissions.allocation_update'),
 | 
			
		||||
                                                            'delete' => trans('server/users.permissions.allocation_delete'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('Startup')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.startup_desc'))
 | 
			
		||||
                                                ->icon('tabler-question-mark')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('startup')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'read' => 'Read',
 | 
			
		||||
                                                            'update' => 'Update',
 | 
			
		||||
                                                            'docker-image' => 'Docker Image',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'read' => trans('server/users.permissions.startup_read'),
 | 
			
		||||
                                                            'update' => trans('server/users.permissions.startup_update'),
 | 
			
		||||
                                                            'docker-image' => trans('server/users.permissions.startup_docker_image'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('Database')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.database_desc'))
 | 
			
		||||
                                                ->icon('tabler-database')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('database')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'read' => 'Read',
 | 
			
		||||
                                                            'create' => 'Create',
 | 
			
		||||
                                                            'update' => 'Update',
 | 
			
		||||
                                                            'delete' => 'Delete',
 | 
			
		||||
                                                            'view_password' => 'View Password',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'read' => trans('server/users.permissions.database_read'),
 | 
			
		||||
                                                            'create' => trans('server/users.permissions.database_create'),
 | 
			
		||||
                                                            'update' => trans('server/users.permissions.database_update'),
 | 
			
		||||
                                                            'delete' => trans('server/users.permissions.database_delete'),
 | 
			
		||||
                                                            'view_password' => trans('server/users.permissions.database_view_password'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('Schedule')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.schedule_desc'))
 | 
			
		||||
                                                ->icon('tabler-clock')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('schedule')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'read' => 'Read',
 | 
			
		||||
                                                            'create' => 'Create',
 | 
			
		||||
                                                            'update' => 'Update',
 | 
			
		||||
                                                            'delete' => 'Delete',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'read' => trans('server/users.permissions.schedule_read'),
 | 
			
		||||
                                                            'create' => trans('server/users.permissions.schedule_create'),
 | 
			
		||||
                                                            'update' => trans('server/users.permissions.schedule_update'),
 | 
			
		||||
                                                            'delete' => trans('server/users.permissions.schedule_delete'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('Settings')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.settings_desc'))
 | 
			
		||||
                                                ->icon('tabler-settings')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('settings')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'rename' => 'Rename',
 | 
			
		||||
                                                            'reinstall' => 'Reinstall',
 | 
			
		||||
                                                            'activity' => 'Activity',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'rename' => trans('server/users.permissions.setting_rename'),
 | 
			
		||||
                                                            'reinstall' => trans('server/users.permissions.setting_reinstall'),
 | 
			
		||||
                                                            'activity' => trans('server/users.permissions.activity_desc'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                    Tabs\Tab::make('Activity')
 | 
			
		||||
                                        ->schema([
 | 
			
		||||
                                            Section::make()
 | 
			
		||||
                                                ->description(trans('server/users.permissions.activity_desc'))
 | 
			
		||||
                                                ->icon('tabler-stack')
 | 
			
		||||
                                                ->schema([
 | 
			
		||||
                                                    CheckboxList::make('activity')
 | 
			
		||||
                                                        ->bulkToggleable()
 | 
			
		||||
                                                        ->label('')
 | 
			
		||||
                                                        ->columns(2)
 | 
			
		||||
                                                        ->options([
 | 
			
		||||
                                                            'read' => 'Read',
 | 
			
		||||
                                                        ])
 | 
			
		||||
                                                        ->descriptions([
 | 
			
		||||
                                                            'read' => trans('server/users.permissions.activity_read'),
 | 
			
		||||
                                                        ]),
 | 
			
		||||
                                                ]),
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                ]),
 | 
			
		||||
 | 
			
		||||
                                ->schema($tabs),
 | 
			
		||||
                        ]),
 | 
			
		||||
                ])
 | 
			
		||||
                ->modalHeading('Invite User')
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ use App\Services\Nodes\NodeJWTService;
 | 
			
		||||
use App\Services\Servers\GetUserPermissionsService;
 | 
			
		||||
use Filament\Widgets\Widget;
 | 
			
		||||
use Illuminate\Support\Arr;
 | 
			
		||||
use Livewire\Attributes\Session;
 | 
			
		||||
use Livewire\Attributes\On;
 | 
			
		||||
 | 
			
		||||
class ServerConsole extends Widget
 | 
			
		||||
@ -26,6 +27,7 @@ class ServerConsole extends Widget
 | 
			
		||||
    public ?User $user = null;
 | 
			
		||||
 | 
			
		||||
    /** @var string[] */
 | 
			
		||||
    #[Session(key: 'server.{server.id}.history')]
 | 
			
		||||
    public array $history = [];
 | 
			
		||||
 | 
			
		||||
    public int $historyIndex = 0;
 | 
			
		||||
@ -130,7 +132,7 @@ class ServerConsole extends Widget
 | 
			
		||||
    #[On('websocket-error')]
 | 
			
		||||
    public function websocketError(): void
 | 
			
		||||
    {
 | 
			
		||||
        AlertBanner::make()
 | 
			
		||||
        AlertBanner::make('websocket_error')
 | 
			
		||||
            ->title('Could not connect to websocket!')
 | 
			
		||||
            ->body('Check your browser console for more details.')
 | 
			
		||||
            ->danger()
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets;
 | 
			
		||||
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Support\RawJs;
 | 
			
		||||
use Filament\Widgets\ChartWidget;
 | 
			
		||||
use Illuminate\Support\Number;
 | 
			
		||||
@ -16,10 +17,19 @@ class ServerCpuChart extends ChartWidget
 | 
			
		||||
 | 
			
		||||
    public ?Server $server = null;
 | 
			
		||||
 | 
			
		||||
    public static function canView(): bool
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getData(): array
 | 
			
		||||
    {
 | 
			
		||||
        $period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
 | 
			
		||||
        $cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))
 | 
			
		||||
            ->slice(-10)
 | 
			
		||||
            ->slice(-$period)
 | 
			
		||||
            ->map(fn ($value, $key) => [
 | 
			
		||||
                'cpu' => Number::format($value, maxPrecision: 2),
 | 
			
		||||
                'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ namespace App\Filament\Server\Widgets;
 | 
			
		||||
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Support\RawJs;
 | 
			
		||||
use Filament\Widgets\ChartWidget;
 | 
			
		||||
use Illuminate\Support\Number;
 | 
			
		||||
@ -16,9 +17,19 @@ class ServerMemoryChart extends ChartWidget
 | 
			
		||||
 | 
			
		||||
    public ?Server $server = null;
 | 
			
		||||
 | 
			
		||||
    public static function canView(): bool
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getData(): array
 | 
			
		||||
    {
 | 
			
		||||
        $memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->slice(-10)
 | 
			
		||||
        $period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
 | 
			
		||||
        $memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))
 | 
			
		||||
            ->slice(-$period)
 | 
			
		||||
            ->map(fn ($value, $key) => [
 | 
			
		||||
                'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2),
 | 
			
		||||
                'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
 | 
			
		||||
 | 
			
		||||
@ -4,61 +4,72 @@ namespace App\Filament\Server\Widgets;
 | 
			
		||||
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Support\RawJs;
 | 
			
		||||
use Filament\Widgets\ChartWidget;
 | 
			
		||||
 | 
			
		||||
class ServerNetworkChart extends ChartWidget
 | 
			
		||||
{
 | 
			
		||||
    protected static ?string $heading = 'Network';
 | 
			
		||||
 | 
			
		||||
    protected static ?string $pollingInterval = '1s';
 | 
			
		||||
 | 
			
		||||
    protected static ?string $maxHeight = '300px';
 | 
			
		||||
    protected static ?string $maxHeight = '200px';
 | 
			
		||||
 | 
			
		||||
    public ?Server $server = null;
 | 
			
		||||
 | 
			
		||||
    public static function canView(): bool
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Server $server */
 | 
			
		||||
        $server = Filament::getTenant();
 | 
			
		||||
 | 
			
		||||
        return !$server->isInConflictState() && !$server->retrieveStatus()->isOffline();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getData(): array
 | 
			
		||||
    {
 | 
			
		||||
        $data = cache()->get("servers.{$this->server->id}.network");
 | 
			
		||||
        $previous = null;
 | 
			
		||||
 | 
			
		||||
        $rx = collect($data)
 | 
			
		||||
            ->slice(-10)
 | 
			
		||||
            ->map(fn ($value, $key) => [
 | 
			
		||||
                'rx' => $value->rx_bytes,
 | 
			
		||||
                'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
 | 
			
		||||
            ])
 | 
			
		||||
            ->all();
 | 
			
		||||
        $period = auth()->user()->getCustomization()['console_graph_period'] ?? 30;
 | 
			
		||||
        $net = collect(cache()->get("servers.{$this->server->id}.network"))
 | 
			
		||||
            ->slice(-$period)
 | 
			
		||||
            ->map(function ($current, $timestamp) use (&$previous) {
 | 
			
		||||
                $net = null;
 | 
			
		||||
 | 
			
		||||
        $tx = collect($data)
 | 
			
		||||
            ->slice(-10)
 | 
			
		||||
            ->map(fn ($value, $key) => [
 | 
			
		||||
                'tx' => $value->rx_bytes,
 | 
			
		||||
                'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
 | 
			
		||||
            ])
 | 
			
		||||
                if ($previous !== null) {
 | 
			
		||||
                    $net = [
 | 
			
		||||
                        'rx' => max(0, $current->rx_bytes - $previous->rx_bytes),
 | 
			
		||||
                        'tx' => max(0, $current->tx_bytes - $previous->tx_bytes),
 | 
			
		||||
                        'timestamp' => Carbon::createFromTimestamp($timestamp, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
 | 
			
		||||
                    ];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $previous = $current;
 | 
			
		||||
 | 
			
		||||
                return $net;
 | 
			
		||||
            })
 | 
			
		||||
            ->all();
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'datasets' => [
 | 
			
		||||
                [
 | 
			
		||||
                    'label' => 'Inbound',
 | 
			
		||||
                    'data' => array_column($rx, 'rx'),
 | 
			
		||||
                    'data' => array_column($net, 'rx'),
 | 
			
		||||
                    'backgroundColor' => [
 | 
			
		||||
                        'rgba(96, 165, 250, 0.3)',
 | 
			
		||||
                        'rgba(100, 255, 105, 0.5)',
 | 
			
		||||
                    ],
 | 
			
		||||
                    'tension' => '0.3',
 | 
			
		||||
                    'fill' => true,
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
                    'label' => 'Outbound',
 | 
			
		||||
                    'data' => array_column($tx, 'tx'),
 | 
			
		||||
                    'data' => array_column($net, 'tx'),
 | 
			
		||||
                    'backgroundColor' => [
 | 
			
		||||
                        'rgba(165, 96, 250, 0.3)',
 | 
			
		||||
                        'rgba(96, 165, 250, 0.3)',
 | 
			
		||||
                    ],
 | 
			
		||||
                    'tension' => '0.3',
 | 
			
		||||
                    'fill' => true,
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
            'labels' => array_column($rx, 'timestamp'),
 | 
			
		||||
            'labels' => array_column($net, 'timestamp'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -69,25 +80,38 @@ class ServerNetworkChart extends ChartWidget
 | 
			
		||||
 | 
			
		||||
    protected function getOptions(): RawJs
 | 
			
		||||
    {
 | 
			
		||||
        // TODO: use "panel.use_binary_prefix" config value
 | 
			
		||||
        return RawJs::make(<<<'JS'
 | 
			
		||||
        {
 | 
			
		||||
            scales: {
 | 
			
		||||
                x: {
 | 
			
		||||
                    grid: {
 | 
			
		||||
                        display: false,
 | 
			
		||||
                    },
 | 
			
		||||
                    ticks: {
 | 
			
		||||
                        display: true,
 | 
			
		||||
                    },
 | 
			
		||||
                    display: false, //debug
 | 
			
		||||
                    display: false,
 | 
			
		||||
                },
 | 
			
		||||
                y: {
 | 
			
		||||
                    min: 0,
 | 
			
		||||
                    ticks: {
 | 
			
		||||
                        display: true,
 | 
			
		||||
                        callback(value) {
 | 
			
		||||
                            const bytes = typeof value === 'string' ? parseInt(value, 10) : value;
 | 
			
		||||
 | 
			
		||||
                            if (bytes < 1) return '0 Bytes';
 | 
			
		||||
 | 
			
		||||
                            const i = Math.floor(Math.log(bytes) / Math.log(1024));
 | 
			
		||||
                            const number = Number((bytes / Math.pow(1024, i)).toFixed(2));
 | 
			
		||||
 | 
			
		||||
                            return `${number} ${['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'][i]}`;
 | 
			
		||||
                        },
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    JS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getHeading(): string
 | 
			
		||||
    {
 | 
			
		||||
        $lastData = collect(cache()->get("servers.{$this->server->id}.network"))->last();
 | 
			
		||||
 | 
			
		||||
        return 'Network - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ namespace App\Filament\Server\Widgets;
 | 
			
		||||
 | 
			
		||||
use App\Enums\ContainerStatus;
 | 
			
		||||
use App\Filament\Server\Components\SmallStatBlock;
 | 
			
		||||
use App\Filament\Server\Components\StatBlock;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use Carbon\CarbonInterface;
 | 
			
		||||
use Filament\Widgets\StatsOverviewWidget;
 | 
			
		||||
@ -19,13 +18,12 @@ class ServerOverview extends StatsOverviewWidget
 | 
			
		||||
    protected function getStats(): array
 | 
			
		||||
    {
 | 
			
		||||
        return [
 | 
			
		||||
            StatBlock::make('Name', $this->server->name)
 | 
			
		||||
                ->description($this->server->description)
 | 
			
		||||
            SmallStatBlock::make('Name', $this->server->name)
 | 
			
		||||
                ->extraAttributes([
 | 
			
		||||
                    'class' => 'overflow-x-auto',
 | 
			
		||||
                ]),
 | 
			
		||||
            StatBlock::make('Status', $this->status()),
 | 
			
		||||
            StatBlock::make('Address', $this->server->allocation->address)
 | 
			
		||||
            SmallStatBlock::make('Status', $this->status()),
 | 
			
		||||
            SmallStatBlock::make('Address', $this->server->allocation->address)
 | 
			
		||||
                ->extraAttributes([
 | 
			
		||||
                    'class' => 'overflow-x-auto',
 | 
			
		||||
                ]),
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ use App\Services\Servers\TransferServerService;
 | 
			
		||||
use Dedoc\Scramble\Attributes\Group;
 | 
			
		||||
use Illuminate\Http\Client\ConnectionException;
 | 
			
		||||
use Illuminate\Http\Response;
 | 
			
		||||
use Illuminate\Support\Arr;
 | 
			
		||||
 | 
			
		||||
#[Group('Server', weight: 4)]
 | 
			
		||||
class ServerManagementController extends ApplicationApiController
 | 
			
		||||
@ -82,15 +83,24 @@ class ServerManagementController extends ApplicationApiController
 | 
			
		||||
        $validatedData = $request->validate([
 | 
			
		||||
            'node_id' => 'required|exists:nodes,id',
 | 
			
		||||
            'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
 | 
			
		||||
            'allocation_additional' => 'nullable',
 | 
			
		||||
            'allocation_additional' => 'nullable|array',
 | 
			
		||||
            'allocation_additional.*' => 'integer|exists:allocations,id',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        if ($this->transferServerService->handle($server, $validatedData)) {
 | 
			
		||||
            // Transfer started
 | 
			
		||||
        if ($this->transferServerService->handle($server, Arr::get($validatedData, 'node_id'), Arr::get($validatedData, 'allocation_id'), Arr::get($validatedData, 'allocation_additional', []))) {
 | 
			
		||||
            /**
 | 
			
		||||
             * Transfer started
 | 
			
		||||
             *
 | 
			
		||||
             * @status 204
 | 
			
		||||
             */
 | 
			
		||||
            return $this->returnNoContent();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Node was not viable
 | 
			
		||||
        /**
 | 
			
		||||
         * Node was not viable
 | 
			
		||||
         *
 | 
			
		||||
         * @status 406
 | 
			
		||||
         */
 | 
			
		||||
        return $this->returnNotAcceptable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -104,7 +114,11 @@ class ServerManagementController extends ApplicationApiController
 | 
			
		||||
    public function cancelTransfer(ServerWriteRequest $request, Server $server): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$transfer = $server->transfer) {
 | 
			
		||||
            // Server is not transferring
 | 
			
		||||
            /**
 | 
			
		||||
             * Server is not transferring
 | 
			
		||||
             *
 | 
			
		||||
             * @status 406
 | 
			
		||||
             */
 | 
			
		||||
            return $this->returnNotAcceptable();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -113,6 +127,11 @@ class ServerManagementController extends ApplicationApiController
 | 
			
		||||
 | 
			
		||||
        $this->daemonServerRepository->setServer($server)->cancelTransfer();
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Transfer cancelled
 | 
			
		||||
         *
 | 
			
		||||
         * @status 204
 | 
			
		||||
         */
 | 
			
		||||
        return $this->returnNoContent();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -172,8 +172,8 @@ class FileController extends ClientApiController
 | 
			
		||||
        Activity::event('server:file.rename')
 | 
			
		||||
            ->property('directory', $request->input('root'))
 | 
			
		||||
            ->property('files', $files)
 | 
			
		||||
            ->property('to', $files['to'])
 | 
			
		||||
            ->property('from', $files['from'])
 | 
			
		||||
            ->property('to', $files[0]['to'])
 | 
			
		||||
            ->property('from', $files[0]['from'])
 | 
			
		||||
            ->log();
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse([], Response::HTTP_NO_CONTENT);
 | 
			
		||||
@ -210,10 +210,12 @@ class FileController extends ClientApiController
 | 
			
		||||
    {
 | 
			
		||||
        $file = $this->fileRepository->setServer($server)->compressFiles(
 | 
			
		||||
            $request->input('root'),
 | 
			
		||||
            $request->input('files')
 | 
			
		||||
            $request->input('files'),
 | 
			
		||||
            $request->input('name')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Activity::event('server:file.compress')
 | 
			
		||||
            ->property('name', $file['name'])
 | 
			
		||||
            ->property('directory', $request->input('root'))
 | 
			
		||||
            ->property('files', $request->input('files'))
 | 
			
		||||
            ->log();
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ use App\Repositories\Daemon\DaemonPowerRepository;
 | 
			
		||||
use App\Http\Controllers\Api\Client\ClientApiController;
 | 
			
		||||
use App\Http\Requests\Api\Client\Servers\SendPowerRequest;
 | 
			
		||||
use Dedoc\Scramble\Attributes\Group;
 | 
			
		||||
use Illuminate\Http\Client\ConnectionException;
 | 
			
		||||
 | 
			
		||||
#[Group('Server', weight: 2)]
 | 
			
		||||
class PowerController extends ClientApiController
 | 
			
		||||
@ -25,6 +26,8 @@ class PowerController extends ClientApiController
 | 
			
		||||
     * Send power action
 | 
			
		||||
     *
 | 
			
		||||
     * Send a power action to a server.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws ConnectionException
 | 
			
		||||
     */
 | 
			
		||||
    public function index(SendPowerRequest $request, Server $server): Response
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -36,26 +36,22 @@ class SettingsController extends ClientApiController
 | 
			
		||||
        $name = $request->input('name');
 | 
			
		||||
        $description = $request->has('description') ? (string) $request->input('description') : $server->description;
 | 
			
		||||
 | 
			
		||||
        $server->name = $name;
 | 
			
		||||
 | 
			
		||||
        if (config('panel.editable_server_descriptions')) {
 | 
			
		||||
            $server->description = $description;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $server->save();
 | 
			
		||||
 | 
			
		||||
        if ($server->name !== $name) {
 | 
			
		||||
            Activity::event('server:settings.rename')
 | 
			
		||||
                ->property(['old' => $server->name, 'new' => $name])
 | 
			
		||||
                ->log();
 | 
			
		||||
            $server->name = $name;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($server->description !== $description) {
 | 
			
		||||
        if ($server->description !== $description && config('panel.editable_server_descriptions')) {
 | 
			
		||||
            Activity::event('server:settings.description')
 | 
			
		||||
                ->property(['old' => $server->description, 'new' => $description])
 | 
			
		||||
                ->log();
 | 
			
		||||
            $server->description = $description;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $server->save();
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse([], Response::HTTP_NO_CONTENT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ class StartupController extends ClientApiController
 | 
			
		||||
        $startup = $this->startupCommandService->handle($server);
 | 
			
		||||
 | 
			
		||||
        return $this->fractal->collection(
 | 
			
		||||
            $server->variables()->orderBy('sort')->where('user_viewable', true)->get()
 | 
			
		||||
            $server->variables()->where('user_viewable', true)->orderBy('sort')->get()
 | 
			
		||||
        )
 | 
			
		||||
            ->transformWith($this->getTransformer(EggVariableTransformer::class))
 | 
			
		||||
            ->addMeta([
 | 
			
		||||
 | 
			
		||||
@ -5,10 +5,8 @@ namespace App\Http\Controllers\Api\Remote;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
use Webmozart\Assert\Assert;
 | 
			
		||||
use App\Models\Server;
 | 
			
		||||
use App\Models\ActivityLog;
 | 
			
		||||
use App\Models\ActivityLogSubject;
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Http\Requests\Api\Remote\ActivityEventRequest;
 | 
			
		||||
 | 
			
		||||
@ -16,8 +14,6 @@ class ActivityProcessingController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    public function __invoke(ActivityEventRequest $request): void
 | 
			
		||||
    {
 | 
			
		||||
        $tz = Carbon::now()->getTimezone();
 | 
			
		||||
 | 
			
		||||
        /** @var \App\Models\Node $node */
 | 
			
		||||
        $node = $request->attributes->get('node');
 | 
			
		||||
 | 
			
		||||
@ -51,11 +47,8 @@ class ActivityProcessingController extends Controller
 | 
			
		||||
            $log = [
 | 
			
		||||
                'ip' => empty($datum['ip']) ? '127.0.0.1' : $datum['ip'],
 | 
			
		||||
                'event' => $datum['event'],
 | 
			
		||||
                'properties' => json_encode($datum['metadata'] ?? []),
 | 
			
		||||
                // We have to change the time to the current timezone due to the way Laravel is handling
 | 
			
		||||
                // the date casting internally. If we just leave it in UTC it ends up getting double-cast
 | 
			
		||||
                // and the time is way off.
 | 
			
		||||
                'timestamp' => $when->setTimezone($tz),
 | 
			
		||||
                'properties' => $datum['metadata'] ?? [],
 | 
			
		||||
                'timestamp' => $when,
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            if ($user = $users->get($datum['user'])) {
 | 
			
		||||
@ -71,19 +64,17 @@ class ActivityProcessingController extends Controller
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($logs as $key => $data) {
 | 
			
		||||
            Assert::isInstanceOf($server = $servers->get($key), Server::class);
 | 
			
		||||
            $server = $servers->get($key);
 | 
			
		||||
            assert($server instanceof Server);
 | 
			
		||||
 | 
			
		||||
            $batch = [];
 | 
			
		||||
            foreach ($data as $datum) {
 | 
			
		||||
                $id = ActivityLog::insertGetId($datum);
 | 
			
		||||
                $batch[] = [
 | 
			
		||||
                    'activity_log_id' => $id,
 | 
			
		||||
                /** @var ActivityLog $activityLog */
 | 
			
		||||
                $activityLog = ActivityLog::forceCreate($datum);
 | 
			
		||||
                $activityLog->subjects()->create([
 | 
			
		||||
                    'subject_id' => $server->id,
 | 
			
		||||
                    'subject_type' => $server->getMorphClass(),
 | 
			
		||||
                ];
 | 
			
		||||
                ]);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            ActivityLogSubject::insert($batch);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,20 @@ class OAuthController extends Controller
 | 
			
		||||
            return redirect()->route('auth.login');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check for errors (https://www.oauth.com/oauth2-servers/server-side-apps/possible-errors/)
 | 
			
		||||
        if ($request->get('error')) {
 | 
			
		||||
            report($request->get('error_description') ?? $request->get('error'));
 | 
			
		||||
 | 
			
		||||
            Notification::make()
 | 
			
		||||
                ->title('Something went wrong')
 | 
			
		||||
                ->body($request->get('error'))
 | 
			
		||||
                ->danger()
 | 
			
		||||
                ->persistent()
 | 
			
		||||
                ->send();
 | 
			
		||||
 | 
			
		||||
            return redirect()->route('auth.login');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $oauthUser = Socialite::driver($driver)->user();
 | 
			
		||||
 | 
			
		||||
        // User is already logged in and wants to link a new OAuth Provider
 | 
			
		||||
@ -53,7 +67,7 @@ class OAuthController extends Controller
 | 
			
		||||
 | 
			
		||||
            $this->updateService->handle($request->user(), ['oauth' => $oauth]);
 | 
			
		||||
 | 
			
		||||
            return redirect(EditProfile::getUrl(['tab' => '-oauth-tab']));
 | 
			
		||||
            return redirect(EditProfile::getUrl(['tab' => '-oauth-tab'], panel: 'app'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,9 @@ namespace App\Http\Middleware;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use App\Exceptions\Http\TwoFactorAuthRequiredException;
 | 
			
		||||
use App\Filament\Pages\Auth\EditProfile;
 | 
			
		||||
use App\Livewire\AlertBanner;
 | 
			
		||||
use App\Models\User;
 | 
			
		||||
 | 
			
		||||
class RequireTwoFactorAuthentication
 | 
			
		||||
{
 | 
			
		||||
@ -14,11 +17,6 @@ class RequireTwoFactorAuthentication
 | 
			
		||||
 | 
			
		||||
    public const LEVEL_ALL = 2;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The route to redirect a user to enable 2FA.
 | 
			
		||||
     */
 | 
			
		||||
    protected string $redirectRoute = '/account';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check the user state on the incoming request to determine if they should be allowed to
 | 
			
		||||
     * proceed or not. This checks if the Panel is configured to require 2FA on an account in
 | 
			
		||||
@ -29,31 +27,37 @@ class RequireTwoFactorAuthentication
 | 
			
		||||
     */
 | 
			
		||||
    public function handle(Request $request, \Closure $next): mixed
 | 
			
		||||
    {
 | 
			
		||||
        /** @var ?User $user */
 | 
			
		||||
        $user = $request->user();
 | 
			
		||||
 | 
			
		||||
        $uri = rtrim($request->getRequestUri(), '/') . '/';
 | 
			
		||||
        $current = $request->route()->getName();
 | 
			
		||||
 | 
			
		||||
        if (!$user || Str::startsWith($uri, ['/auth/']) || Str::startsWith($current, ['auth.', 'account.'])) {
 | 
			
		||||
        if (!$user || Str::startsWith($uri, ['/auth/', '/profile']) || Str::startsWith($current, ['auth.', 'account.', 'filament.app.auth.'])) {
 | 
			
		||||
            return $next($request);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /** @var \App\Models\User $user */
 | 
			
		||||
        $level = (int) config('panel.auth.2fa_required');
 | 
			
		||||
        // If this setting is not configured, or the user is already using 2FA then we can just
 | 
			
		||||
        // send them right through, nothing else needs to be checked.
 | 
			
		||||
        //
 | 
			
		||||
        // If the level is set as admin and the user is not an admin, pass them through as well.
 | 
			
		||||
 | 
			
		||||
        if ($level === self::LEVEL_NONE || $user->use_totp) {
 | 
			
		||||
            // If this setting is not configured, or the user is already using 2FA then we can just send them right through, nothing else needs to be checked.
 | 
			
		||||
            return $next($request);
 | 
			
		||||
        } elseif ($level === self::LEVEL_ADMIN && !$user->isRootAdmin()) {
 | 
			
		||||
        } elseif ($level === self::LEVEL_ADMIN && !$user->isAdmin()) {
 | 
			
		||||
            // If the level is set as admin and the user is not an admin, pass them through as well.
 | 
			
		||||
            return $next($request);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // For API calls return an exception which gets rendered nicely in the API response.
 | 
			
		||||
        // For API calls return an exception which gets rendered nicely in the API response...
 | 
			
		||||
        if ($request->isJson() || Str::startsWith($uri, '/api/')) {
 | 
			
		||||
            throw new TwoFactorAuthRequiredException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return redirect()->to($this->redirectRoute);
 | 
			
		||||
        // ... otherwise display banner and redirect to profile
 | 
			
		||||
        AlertBanner::make('2fa_must_be_enabled')
 | 
			
		||||
            ->body(trans('auth.2fa_must_be_enabled'))
 | 
			
		||||
            ->warning()
 | 
			
		||||
            ->send();
 | 
			
		||||
 | 
			
		||||
        return redirect(EditProfile::getUrl(['tab' => '-2fa-tab'], panel: 'app'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,26 +18,7 @@ class StoreNodeRequest extends ApplicationApiRequest
 | 
			
		||||
     */
 | 
			
		||||
    public function rules(?array $rules = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return collect($rules ?? Node::getRules())->only([
 | 
			
		||||
            'public',
 | 
			
		||||
            'name',
 | 
			
		||||
            'description',
 | 
			
		||||
            'fqdn',
 | 
			
		||||
            'scheme',
 | 
			
		||||
            'behind_proxy',
 | 
			
		||||
            'maintenance_mode',
 | 
			
		||||
            'memory',
 | 
			
		||||
            'memory_overallocate',
 | 
			
		||||
            'disk',
 | 
			
		||||
            'disk_overallocate',
 | 
			
		||||
            'cpu',
 | 
			
		||||
            'cpu_overallocate',
 | 
			
		||||
            'upload_size',
 | 
			
		||||
            'daemon_listen',
 | 
			
		||||
            'daemon_sftp',
 | 
			
		||||
            'daemon_sftp_alias',
 | 
			
		||||
            'daemon_base',
 | 
			
		||||
        ])->mapWithKeys(function ($value, $key) {
 | 
			
		||||
        return collect($rules ?? Node::getRules())->mapWithKeys(function ($value, $key) {
 | 
			
		||||
            return [snake_case($key) => $value];
 | 
			
		||||
        })->toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
 | 
			
		||||
     */
 | 
			
		||||
    public function rules(): array
 | 
			
		||||
    {
 | 
			
		||||
        $rules = Server::getRulesForUpdate($this->parameter('server', Server::class));
 | 
			
		||||
        $rules = $this->route() ? Server::getRulesForUpdate($this->parameter('server', Server::class)) : Server::getRules();
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'allocation' => $rules['allocation_id'],
 | 
			
		||||
@ -26,13 +26,17 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
 | 
			
		||||
            'limits.threads' => $this->requiredToOptional('threads', $rules['threads'], true),
 | 
			
		||||
            'limits.disk' => $this->requiredToOptional('disk', $rules['disk'], true),
 | 
			
		||||
 | 
			
		||||
            // Legacy rules to maintain backwards compatable API support without requiring
 | 
			
		||||
            // a major version bump.
 | 
			
		||||
            // Deprecated - use limits.memory
 | 
			
		||||
            'memory' => $this->requiredToOptional('memory', $rules['memory']),
 | 
			
		||||
            // Deprecated - use limits.swap
 | 
			
		||||
            'swap' => $this->requiredToOptional('swap', $rules['swap']),
 | 
			
		||||
            // Deprecated - use limits.io
 | 
			
		||||
            'io' => $this->requiredToOptional('io', $rules['io']),
 | 
			
		||||
            // Deprecated - use limits.cpu
 | 
			
		||||
            'cpu' => $this->requiredToOptional('cpu', $rules['cpu']),
 | 
			
		||||
            // Deprecated - use limits.threads
 | 
			
		||||
            'threads' => $this->requiredToOptional('threads', $rules['threads']),
 | 
			
		||||
            // Deprecated - use limits.disk
 | 
			
		||||
            'disk' => $this->requiredToOptional('disk', $rules['disk']),
 | 
			
		||||
 | 
			
		||||
            'add_allocations' => 'bail|array',
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ class UpdateServerDetailsRequest extends ServerWriteRequest
 | 
			
		||||
     */
 | 
			
		||||
    public function rules(): array
 | 
			
		||||
    {
 | 
			
		||||
        $rules = Server::getRulesForUpdate($this->parameter('server', Server::class));
 | 
			
		||||
        $rules = $this->route() ? Server::getRulesForUpdate($this->parameter('server', Server::class)) : Server::getRules();
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'external_id' => $rules['external_id'],
 | 
			
		||||
 | 
			
		||||
@ -17,12 +17,12 @@ class UpdateServerStartupRequest extends ApplicationApiRequest
 | 
			
		||||
     */
 | 
			
		||||
    public function rules(): array
 | 
			
		||||
    {
 | 
			
		||||
        $data = Server::getRulesForUpdate($this->parameter('server', Server::class));
 | 
			
		||||
        $rules = $this->route() ? Server::getRulesForUpdate($this->parameter('server', Server::class)) : Server::getRules();
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'startup' => 'sometimes|string',
 | 
			
		||||
            'environment' => 'present|array',
 | 
			
		||||
            'egg' => $data['egg_id'],
 | 
			
		||||
            'egg' => $rules['egg_id'],
 | 
			
		||||
            'image' => 'sometimes|string',
 | 
			
		||||
            'skip_scripts' => 'present|boolean',
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ class CompressFilesRequest extends ClientApiRequest
 | 
			
		||||
            'root' => 'sometimes|nullable|string',
 | 
			
		||||
            'files' => 'required|array',
 | 
			
		||||
            'files.*' => 'string',
 | 
			
		||||
            'name' => 'sometimes|nullable|string',
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -44,9 +44,8 @@ final class AlertBanner implements Wireable
 | 
			
		||||
 | 
			
		||||
    public static function fromLivewire(mixed $value): AlertBanner
 | 
			
		||||
    {
 | 
			
		||||
        $static = AlertBanner::make();
 | 
			
		||||
        $static = AlertBanner::make($value['id']);
 | 
			
		||||
 | 
			
		||||
        $static->id($value['id']);
 | 
			
		||||
        $static->title($value['title']);
 | 
			
		||||
        $static->body($value['body']);
 | 
			
		||||
        $static->status($value['status']);
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
namespace App\Livewire;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Contracts\View\View;
 | 
			
		||||
use Livewire\Attributes\On;
 | 
			
		||||
use Livewire\Component;
 | 
			
		||||
 | 
			
		||||
class AlertBannerContainer extends Component
 | 
			
		||||
@ -16,6 +17,7 @@ class AlertBannerContainer extends Component
 | 
			
		||||
        $this->pullFromSession();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[On('alertBannerSent')]
 | 
			
		||||
    public function pullFromSession(): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach (session()->pull('alert-banners', []) as $alertBanner) {
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ class DatabaseStep
 | 
			
		||||
        'sqlite' => 'SQLite',
 | 
			
		||||
        'mariadb' => 'MariaDB',
 | 
			
		||||
        'mysql' => 'MySQL',
 | 
			
		||||
        'pgsql' => 'PostgreSQL',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    public static function make(PanelInstaller $installer): Step
 | 
			
		||||
@ -39,15 +40,24 @@ class DatabaseStep
 | 
			
		||||
                    ->afterStateUpdated(function ($state, Set $set, Get $get) {
 | 
			
		||||
                        $set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel');
 | 
			
		||||
 | 
			
		||||
                        if ($state === 'sqlite') {
 | 
			
		||||
                            $set('env_database.DB_HOST', null);
 | 
			
		||||
                            $set('env_database.DB_PORT', null);
 | 
			
		||||
                            $set('env_database.DB_USERNAME', null);
 | 
			
		||||
                            $set('env_database.DB_PASSWORD', null);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
 | 
			
		||||
                            $set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306');
 | 
			
		||||
                            $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
 | 
			
		||||
                        switch ($state) {
 | 
			
		||||
                            case 'sqlite':
 | 
			
		||||
                                $set('env_database.DB_HOST', null);
 | 
			
		||||
                                $set('env_database.DB_PORT', null);
 | 
			
		||||
                                $set('env_database.DB_USERNAME', null);
 | 
			
		||||
                                $set('env_database.DB_PASSWORD', null);
 | 
			
		||||
                                break;
 | 
			
		||||
                            case 'mariadb':
 | 
			
		||||
                            case 'mysql':
 | 
			
		||||
                                $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
 | 
			
		||||
                                $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
 | 
			
		||||
                                $set('env_database.DB_PORT', '3306');
 | 
			
		||||
                                break;
 | 
			
		||||
                            case 'pgsql':
 | 
			
		||||
                                $set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
 | 
			
		||||
                                $set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
 | 
			
		||||
                                $set('env_database.DB_PORT', '5432');
 | 
			
		||||
                                break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }),
 | 
			
		||||
                TextInput::make('env_database.DB_DATABASE')
 | 
			
		||||
@ -114,7 +124,6 @@ class DatabaseStep
 | 
			
		||||
                'database' => $database,
 | 
			
		||||
                'username' => $username,
 | 
			
		||||
                'password' => $password,
 | 
			
		||||
                'charset' => 'utf8mb4',
 | 
			
		||||
                'collation' => 'utf8mb4_unicode_ci',
 | 
			
		||||
                'strict' => true,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ use App\Traits\HasValidation;
 | 
			
		||||
use Carbon\Carbon;
 | 
			
		||||
use Illuminate\Support\Facades\Event;
 | 
			
		||||
use App\Events\ActivityLogged;
 | 
			
		||||
use Filament\Facades\Filament;
 | 
			
		||||
use Filament\Support\Contracts\HasIcon;
 | 
			
		||||
use Filament\Support\Contracts\HasLabel;
 | 
			
		||||
use Illuminate\Database\Eloquent\Builder;
 | 
			
		||||
@ -120,11 +121,6 @@ class ActivityLog extends Model implements HasIcon, HasLabel
 | 
			
		||||
        return $builder->whereMorphedTo('actor', $actor);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns models to be pruned.
 | 
			
		||||
     *
 | 
			
		||||
     * @see https://laravel.com/docs/9.x/eloquent#pruning-models
 | 
			
		||||
     */
 | 
			
		||||
    public function prunable(): Builder
 | 
			
		||||
    {
 | 
			
		||||
        if (is_null(config('activity.prune_days'))) {
 | 
			
		||||
@ -134,10 +130,6 @@ class ActivityLog extends Model implements HasIcon, HasLabel
 | 
			
		||||
        return static::where('timestamp', '<=', Carbon::now()->subDays(config('activity.prune_days')));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Boots the model event listeners. This will trigger an activity log event every
 | 
			
		||||
     * time a new model is inserted which can then be captured and worked with as needed.
 | 
			
		||||
     */
 | 
			
		||||
    protected static function boot(): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::boot();
 | 
			
		||||
@ -181,9 +173,11 @@ class ActivityLog extends Model implements HasIcon, HasLabel
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $avatarUrl = Filament::getUserAvatarUrl($user);
 | 
			
		||||
 | 
			
		||||
        return "
 | 
			
		||||
            <div style='display: flex; align-items: center;'>
 | 
			
		||||
                <img width='50px' height='50px' src='{$user->getFilamentAvatarUrl()}' style='margin-right: 15px' />
 | 
			
		||||
                <img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px' />
 | 
			
		||||
 | 
			
		||||
                <div>
 | 
			
		||||
                    <p>$user->username — $this->event</p>
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user