Compare commits

...

68 Commits

Author SHA1 Message Date
notCharles
40819cf171 Use correct action 2024-06-29 18:02:53 -04:00
Boy132
133b94ab08 Add missing user timezone stuff (#446) 2024-06-29 23:42:46 +02:00
Charles
82c0568129 Reduce Reuse (#443)
* Reduce Reuse

Reduce the repetitiveness of \Form\Component\Blah along with all the others...

* PHPStan Fix
2024-06-29 17:38:18 -04:00
MartinOscar
75d35e6ee8 Add ability to create User on Create Server page (#442)
* Add ability to create a User on Create Server page

* pint

* Fix creating user without password

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-06-29 17:18:42 -04:00
Boy132
2a740b43e6 Fix schedules running every minute (second try) (#444) 2024-06-29 23:16:28 +02:00
Charles
818a8a42ad Closes #439 (#440)
* Update readme.md

* Update readme.md

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2024-06-27 19:09:50 -04:00
Lance Pioch
67dbf772d5 Separate these out in the navigation (#434) 2024-06-27 05:54:21 -04:00
Lance Pioch
efb834c8f7 Combine Server states and statuses and resolve #362 (#417)
* Simplify states and statuses and resolve #362

# Conflicts:
#	app/Models/Server.php

* Move Random button...

Moves button to the info tab

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-06-25 20:30:06 -04:00
Lance Pioch
cf37994c3b Allow user to switch time zones (#332)
* Description not required

* Overwrite to use user’s time zone

* Allow users to change time zones

* Update app/Filament/Resources/UserResource/Pages/EditProfile.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

* Pint fix

---------

Co-authored-by: Charles <charles@pelican.dev>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-06-25 20:17:08 -04:00
Boy132
fc92a87993 Add connection test to database hosts (#410)
* add connection test to database hosts

* fix phpstan
2024-06-25 18:42:55 -04:00
Lance Pioch
f459987458 Allow manual (force) updates to eggs (#427)
* Update labels

* Add force imports

* Not multiple

* pint + changes

---------

Co-authored-by: Charles <charles@pelican.dev>
2024-06-25 15:37:12 -04:00
MartinOscar
5290b8f8bb Update ListUsers Prevent bulkdelete of yourself/last admin (#425)
* Update ListUsers.php

* Update ListUsers.php

* Update app/Filament/Resources/UserResource/Pages/ListUsers.php

---------

Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2024-06-24 21:41:42 -04:00
MartinOscar
e08cbdecd4 Update EnvironmentWriterTrait to allow empty string in CLI (#421) 2024-06-24 13:52:52 -04:00
Boy132
70c31eef8f Refactor UserTransformers (#423)
* remove AccountTransformer and update UserTransformer (client api) to match UserTransformer (application api)

* rename "toVueObject"

* fix tests

* forgot to rename this

* backwards compat

* fix tests
2024-06-23 16:33:18 +02:00
Boy132
5409532ca1 Fix the fix (#424) 2024-06-23 16:23:11 +02:00
Lance Pioch
a1190c12e0 Add required to editing server variables and fix #413 (#415)
* Add required to editing server variables

* Misplaced
2024-06-21 16:17:44 -04:00
notCharles
42ca4e7fba This never happened... 2024-06-20 16:15:56 -04:00
notCharles
d6b71885ec Add env. to egg upgrader 2024-06-20 16:00:17 -04:00
Boy132
7b0a15e746 Remove hard coded queue name for RunTaskJob (#420) 2024-06-19 16:04:00 +02:00
Boy132
7813b6060c Make oauth nullable & remove middleware from oauth callback (#418)
* make oauth nullable

* fix oauth callback middleware
2024-06-18 22:05:08 +02:00
Charles
c431775b7e [Create Server] Fix 500 when changing egg
When changing from one egg to nothing, a 500 is displayed due to it expecting startup to have a value
2024-06-17 12:24:52 -04:00
Lance Pioch
6692942f6f Group servers (#412)
* Group servers

# Conflicts:
#	app/Filament/Resources/ServerResource/Pages/ListServers.php

* Can be null apparently

* pint

---------

Co-authored-by: Charles <charles@pelican.dev>
2024-06-17 12:12:56 -04:00
MartinOscar
276b51f477 Remove locationId in MakeNodeCommand (#405)
* Concat + Default

* Concat + Default + Enforce scheme

* fix typo

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-06-17 08:51:04 -04:00
MartinOscar
d4eecdd53d Update OAuth migration (#409) 2024-06-17 07:03:36 -04:00
Senna
d7316c4dfe Dashboard update. Update section (#390)
* Created command

* Pint Fixes

* Removed old upgrade command translations

* Update to Dashboard and linting Dashboard view

* Pint Fixes

* A few small improvements

* Delete modifications to upgrade command

* Revert "Removed old upgrade command translations"

This reverts commit 31315a0d9e.

* Pint Fixes

* Boy132's Suggestions
2024-06-16 17:54:49 -04:00
notCharles
011579451d Merge branch 'main' of https://github.com/pelican-dev/panel 2024-06-16 14:21:33 -04:00
notCharles
6b5b480902 Update database section on EditServer 2024-06-16 14:21:25 -04:00
MartinOscar
87dc8066c9 Update required (#401) 2024-06-16 14:01:27 -04:00
MartinOscar
aa08e774a1 Fix varchar(191) by replacing with 255 #135 (#376)
* Add Nullable

* Edit filament & AppServiceProvider

* Pint

* Patch tests

* Actually patching tests

* Actually patching tests

* Remove length

* Remove defaultStringLength

* Let’s see the differences

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2024-06-16 13:56:18 -04:00
notCharles
482e8ed6b2 Add Databases to Edit Server 2024-06-16 13:50:28 -04:00
Boy132
59bbb63739 Fix queue worker file when using redis (#399) 2024-06-15 23:20:08 +02:00
Charles
f4c3c89c17 Also add that here
Prevent 500's on server create
2024-06-15 12:36:15 -04:00
Charles
fe4e6271fb Set minValue
Closes #397
2024-06-15 12:33:33 -04:00
Boy132
8ee5d6aabd Add missing "search" translations (#393) 2024-06-15 14:46:10 +02:00
Boy132
42ecd2951d Update p:info command (#389) 2024-06-14 17:17:49 +02:00
Boy132
7a6edab79a Remove unnecessary json_encode in oauthcontrollers (#391) 2024-06-14 17:17:34 +02:00
MartinOscar
4f43e9171a Rename OauthController.php to OAuthController.php (#388)
Fixes Class App\Http\Controllers\Base\OAuthController located in ./app/Http/Controllers/Base/OauthController.php does not comply with psr-4 autoloading standard (rule: App\ => ./app). Skipping.
2024-06-13 23:52:53 -04:00
Boy132
5a3c606627 Add OAuth backend (#386)
* add socialite backend

* fix redirect url

* small cleanup

* fix "oauth" type

* changes from review
2024-06-13 21:06:31 +02:00
Boy132
6916b89638 Fill startup & image from egg if missing in request (#384) 2024-06-13 15:48:36 +02:00
Boy132
0da184c56e Get latest Panel & Wings version from github api (#379)
* get latest panel % wings version from github api

* fix tests

* remove unused CdnVersionFetchingException
2024-06-13 08:23:45 +02:00
Boy132
ce1163d387 Hide task delay on frontend if queue driver is set to sync (#375) 2024-06-13 08:23:24 +02:00
Boy132
cd4fc1a95d Add config variable to change between MiB and MB (#374) 2024-06-13 08:22:29 +02:00
Boy132
0c0b468525 Change allowed_ips to non-nullable (#373)
* change `allowed_ips` to non nullable

* fix default value

* show "allowed_ips" input
2024-06-13 08:21:56 +02:00
notCharles
12518bc5d6 Allow more searchable
Allow more columns to be searchable.
2024-06-12 19:30:02 -04:00
Boy132
7c829fb9cf Fix egg export (#380) 2024-06-12 18:18:24 +02:00
Boy132
61f3e965ba Refactor egg services (#358)
* combine importer and updateimport

* integrate egg parser into importer

* remove EggCreationService and EggUpdateService

* run pint

* revert change to composer.json

* use egg exporter directly instead of old admin route
2024-06-11 21:01:31 +02:00
Boy132
10796f8916 Dedicated MariaDB driver (#365)
* dedicated tests for mariadb

* fix migrations

* update database config

* update database setup command
2024-06-11 21:01:14 +02:00
Djordy Koert
1d66d4c320 PHPStan workflow + PHPStan fixes (#339)
* add PHPStan to workflow

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: Djordy <djordy@lap-0394-djordy-koert.local>
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2024-06-11 15:00:51 -04:00
Lance Pioch
e95cd0cd98 Update bounties.md 2024-06-11 14:58:42 -04:00
MartinOscar
46a24a087b Update CreateServer.php (#364) 2024-06-11 14:36:22 -04:00
MartinOscar
f216376265 Update EditProfile (#368)
* Update EditProfile.php
2024-06-11 14:32:55 -04:00
Lance Pioch
6d6b50c27d Create bounties.md 2024-06-11 12:48:04 -04:00
MartinOscar
58bfa12280 Cleanup unused vars (#366)
* Update app/Exceptions/Handler.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-06-11 12:22:37 -04:00
MartinOscar
8e5660a1b9 Add default for Automatic Allocation in CreateNode (#367)
* Update CreateNode.php
* Update app/Filament/Resources/NodeResource/Pages/CreateNode.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-06-11 12:21:51 -04:00
MartinOscar
beac4cd3f6 Update AllocationsRelationManager.php (#369)
* Update AllocationsRelationManager.php

* Update app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php

---------

Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2024-06-11 12:21:13 -04:00
kubi
9184441763 Update release workflow for new parameter format (#363)
action-gh-release combined their artifact uploader action with their create release action.  Combine these into a single step.
2024-06-10 15:08:18 -07:00
kubi
3ac23d1514 Update version of upload release actions in release workflow 2024-06-10 14:55:14 -07:00
kubi
6295ea34de Update tests to use mariadb 10.3
Update version of mariadb used by tests since this is our minimum required version.
2024-06-10 14:45:32 -07:00
kubi
3cadbbc60c Run build on commits 2024-06-10 14:41:02 -07:00
kubi
60c5f826d6 Run build and tests on commits 2024-06-10 14:32:36 -07:00
Boy132
1047e8f948 Make egg list searchable and sortable (#359)
* make egg list searchable and sortable

* run pint
2024-06-10 09:11:12 -04:00
Lance Pioch
f3501d8b14 Merge pull request #343 from Boy132/phpstan-fixes
Fix remaining phpstan issues for #339
2024-06-09 15:13:44 -04:00
Charles
9114685680 Use Wizard for server/node create pages (#352)
* Update create server flow

* Update create node & buttons

* Remove duplicate

* Composer Update

Update some of the packages <3

* Small adjustments

* pint

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2024-06-09 15:07:33 -04:00
notCharles
8080435eca It's Late... 2024-06-07 22:28:53 -04:00
notCharles
c5824ff26c Whoops.... Fix env replacement...
Somehow this got copy pasta'd and yeh.... its not right...
2024-06-07 22:18:12 -04:00
Boy132
6b249b9545 fix tests 2024-06-07 09:17:10 +02:00
Boy132
70fc84309f revert some changes in EditProfile 2024-06-07 09:11:40 +02:00
Boy132
f43fb985a2 fix phpstan in Node and EditProfile 2024-06-07 08:59:00 +02:00
137 changed files with 4204 additions and 2718 deletions

View File

@@ -1,6 +1,9 @@
name: Build
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'

View File

@@ -1,6 +1,9 @@
name: Tests
on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
@@ -13,7 +16,7 @@ jobs:
fail-fast: false
matrix:
php: [8.2, 8.3]
database: ["mariadb:10.2", "mysql:8"]
database: ["mysql:8"]
services:
database:
image: ${{ matrix.database }}
@@ -78,6 +81,78 @@ jobs:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
mariadb:
name: MariaDB
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3]
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: testing
ports:
- 3306
options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
APP_ENVIRONMENT_ONLY: "true"
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: mariadb
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: root
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --prefer-dist
- name: Unit tests
run: vendor/bin/phpunit tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/phpunit tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
sqlite:
name: SQLite
runs-on: ubuntu-latest

View File

@@ -6,8 +6,8 @@ on:
- '**'
jobs:
lint:
name: Lint
pint:
name: Pint
runs-on: ubuntu-latest
steps:
- name: Code Checkout
@@ -16,7 +16,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.2"
php-version: "8.3"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -29,3 +29,26 @@ jobs:
- name: Pint
run: vendor/bin/pint --test
phpstan:
name: PHPStan
runs-on: ubuntu-latest
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Setup .env
run: cp .env.example .env
- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist
- name: PHPStan
run: vendor/bin/phpstan --memory-limit=-1

View File

@@ -54,31 +54,12 @@ jobs:
- name: Create release
id: create_release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
- name: Upload release archive
id: upload-release-archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: panel.tar.gz
asset_name: panel.tar.gz
asset_content_type: application/gzip
- name: Upload release checksum
id: upload-release-checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./checksum.txt
asset_name: checksum.txt
asset_content_type: text/plain
files: |
panel.tar.gz
checksum.txt

View File

@@ -13,6 +13,7 @@ class DatabaseSettingsCommand extends Command
public const DATABASE_DRIVERS = [
'sqlite' => 'SQLite (recommended)',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
];
@@ -21,10 +22,10 @@ class DatabaseSettingsCommand extends Command
protected $signature = 'p:environment:database
{--driver= : The database driver backend to use.}
{--database= : The database to use.}
{--host= : The connection address for the MySQL server.}
{--port= : The connection port for the MySQL server.}
{--username= : Username to use when connecting to the MySQL server.}
{--password= : Password to use for the MySQL database.}';
{--host= : The connection address for the MySQL/ MariaDB server.}
{--port= : The connection port for the MySQL/ MariaDB server.}
{--username= : Username to use when connecting to the MySQL/ MariaDB server.}
{--password= : Password to use for the MySQL/ MariaDB database.}';
protected array $variables = [];
@@ -82,7 +83,20 @@ class DatabaseSettingsCommand extends Command
}
try {
$this->testMySQLConnection();
// Test connection
config()->set('database.connections._panel_command_test', [
'driver' => 'mysql',
'host' => $this->variables['DB_HOST'],
'port' => $this->variables['DB_PORT'],
'database' => $this->variables['DB_DATABASE'],
'username' => $this->variables['DB_USERNAME'],
'password' => $this->variables['DB_PASSWORD'],
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
$this->database->connection('_panel_command_test')->getPdo();
} catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(__('commands.database_settings.DB_error_2'));
@@ -93,6 +107,66 @@ class DatabaseSettingsCommand extends Command
return $this->handle();
}
return 1;
}
} elseif ($this->variables['DB_CONNECTION'] === 'mariadb') {
$this->output->note(__('commands.database_settings.DB_HOST_note'));
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
'Database Host',
config('database.connections.mariadb.host', '127.0.0.1')
);
$this->variables['DB_PORT'] = $this->option('port') ?? $this->ask(
'Database Port',
config('database.connections.mariadb.port', 3306)
);
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
'Database Name',
config('database.connections.mariadb.database', 'panel')
);
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
'Database Username',
config('database.connections.mariadb.username', 'pelican')
);
$askForMariaDBPassword = true;
if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) {
$this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password');
$askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
}
if ($askForMariaDBPassword) {
$this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password');
}
try {
// Test connection
config()->set('database.connections._panel_command_test', [
'driver' => 'mariadb',
'host' => $this->variables['DB_HOST'],
'port' => $this->variables['DB_PORT'],
'database' => $this->variables['DB_DATABASE'],
'username' => $this->variables['DB_USERNAME'],
'password' => $this->variables['DB_PASSWORD'],
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
$this->database->connection('_panel_command_test')->getPdo();
} catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(__('commands.database_settings.DB_error_2'));
if ($this->confirm(__('commands.database_settings.go_back'))) {
$this->database->disconnect('_panel_command_test');
return $this->handle();
}
return 1;
}
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
@@ -108,24 +182,4 @@ class DatabaseSettingsCommand extends Command
return 0;
}
/**
* Test that we can connect to the provided MySQL instance and perform a selection.
*/
private function testMySQLConnection()
{
config()->set('database.connections._panel_command_test', [
'driver' => 'mysql',
'host' => $this->variables['DB_HOST'],
'port' => $this->variables['DB_PORT'],
'database' => $this->variables['DB_DATABASE'],
'username' => $this->variables['DB_USERNAME'],
'password' => $this->variables['DB_PASSWORD'],
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
$this->database->connection('_panel_command_test')->getPdo();
}
}

View File

@@ -24,7 +24,7 @@ class QueueWorkerServiceCommand extends Command
$fileExists = file_exists($path);
if ($fileExists && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
$this->line('Creation of queue worker service file aborted because serive file already exists.');
$this->line('Creation of queue worker service file aborted because service file already exists.');
return;
}
@@ -32,7 +32,8 @@ class QueueWorkerServiceCommand extends Command
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
$afterRedis = $this->option('use-redis') ? '\nAfter=redis-server.service' : '';
$afterRedis = $this->option('use-redis') ? '
After=redis-server.service' : '';
$basePath = base_path();

View File

@@ -7,12 +7,12 @@ use App\Services\Helpers\SoftwareVersionService;
class InfoCommand extends Command
{
protected $description = 'Displays the application, database, and email configurations along with the panel version.';
protected $description = 'Displays the application, database, email and backup configurations along with the panel version.';
protected $signature = 'p:info';
/**
* VersionCommand constructor.
* InfoCommand constructor.
*/
public function __construct(private SoftwareVersionService $versionService)
{
@@ -33,38 +33,69 @@ class InfoCommand extends Command
$this->output->title('Application Configuration');
$this->table([], [
['Environment', $this->formatText(config('app.env'), config('app.env') === 'production' ?: 'bg=red')],
['Debug Mode', $this->formatText(config('app.debug') ? 'Yes' : 'No', !config('app.debug') ?: 'bg=red')],
['Installation URL', config('app.url')],
['Environment', config('app.env') === 'production' ? config('app.env') : $this->formatText(config('app.env'), 'bg=red')],
['Debug Mode', config('app.debug') ? $this->formatText('Yes', 'bg=red') : 'No'],
['Application Name', config('app.name')],
['Application URL', config('app.url')],
['Installation Directory', base_path()],
['Cache Driver', config('cache.default')],
['Queue Driver', config('queue.default')],
['Queue Driver', config('queue.default') === 'sync' ? $this->formatText(config('queue.default'), 'bg=red') : config('queue.default')],
['Session Driver', config('session.driver')],
['Filesystem Driver', config('filesystems.default')],
['Default Theme', config('themes.active')],
], 'compact');
$this->output->title('Database Configuration');
$driver = config('database.default');
$this->table([], [
['Driver', $driver],
['Host', config("database.connections.$driver.host")],
['Port', config("database.connections.$driver.port")],
['Database', config("database.connections.$driver.database")],
['Username', config("database.connections.$driver.username")],
], 'compact');
if ($driver === 'sqlite') {
$this->table([], [
['Driver', $driver],
['Database', config("database.connections.$driver.database")],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
['Host', config("database.connections.$driver.host")],
['Port', config("database.connections.$driver.port")],
['Database', config("database.connections.$driver.database")],
['Username', config("database.connections.$driver.username")],
], 'compact');
}
// TODO: Update this to handle other mail drivers
$this->output->title('Email Configuration');
$this->table([], [
['Driver', config('mail.default')],
['Host', config('mail.mailers.smtp.host')],
['Port', config('mail.mailers.smtp.port')],
['Username', config('mail.mailers.smtp.username')],
['From Address', config('mail.from.address')],
['From Name', config('mail.from.name')],
['Encryption', config('mail.mailers.smtp.encryption')],
], 'compact');
$driver = config('mail.default');
if ($driver === 'smtp') {
$this->table([], [
['Driver', $driver],
['Host', config("mail.mailers.$driver.host")],
['Port', config("mail.mailers.$driver.port")],
['Username', config("mail.mailers.$driver.username")],
['Encryption', config("mail.mailers.$driver.encryption")],
['From Address', config('mail.from.address')],
['From Name', config('mail.from.name')],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
['From Address', config('mail.from.address')],
['From Name', config('mail.from.name')],
], 'compact');
}
$this->output->title('Backup Configuration');
$driver = config('backups.default');
if ($driver === 's3') {
$this->table([], [
['Driver', $driver],
['Region', config("backups.disks.$driver.region")],
['Bucket', config("backups.disks.$driver.bucket")],
['Endpoint', config("backups.disks.$driver.endpoint")],
['Use path style endpoint', config("backups.disks.$driver.use_path_style_endpoint") ? 'Yes' : 'No'],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
], 'compact');
}
}
/**

View File

@@ -57,19 +57,19 @@ class MakeNodeCommand extends Command
$data['public'] = $this->option('public') ?? $this->confirm(__('commands.make_node.public'), true);
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm(__('commands.make_node.behind_proxy'));
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm(__('commands.make_node.maintenance_mode'));
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'));
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'));
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'));
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'));
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'));
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'));
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100');
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'), '0');
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'), '-1');
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'), '0');
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'), '-1');
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'), '0');
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'), '-1');
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '256');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(__('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
$node = $this->creationService->handle($data);
$this->line(__('commands.make_node.succes1') . $data['name'] . __('commands.make_node.succes2') . $node->id . '.');
$this->line(__('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
}
}

View File

@@ -24,7 +24,7 @@ class ProcessRunnableCommand extends Command
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
->where('is_active', true)
->where('is_processing', false)
->whereDate('next_run_at', '<=', Carbon::now()->toDateTimeString())
->where('next_run_at', '<=', Carbon::now()->toDateTimeString())
->get();
if ($schedules->count() < 1) {

View File

@@ -12,6 +12,7 @@ enum ContainerStatus: string
case Paused = 'paused';
case Dead = 'dead';
case Removing = 'removing';
case Offline = 'offline';
// HTTP Based
case Missing = 'missing';
@@ -27,6 +28,7 @@ enum ContainerStatus: string
self::Dead => 'tabler-heart-x',
self::Removing => 'tabler-heart-down',
self::Missing => 'tabler-heart-question',
self::Offline => 'tabler-heart-bolt',
};
}
@@ -41,6 +43,7 @@ enum ContainerStatus: string
self::Dead => 'danger',
self::Removing => 'warning',
self::Missing => 'danger',
self::Offline => 'gray',
};
}
}

View File

@@ -215,7 +215,7 @@ class Handler extends ExceptionHandler
->map(fn ($trace) => Arr::except($trace, ['args']))
->all(),
'previous' => Collection::make($this->extractPrevious($e))
->map(fn ($exception) => $e->getTrace())
->map(fn ($exception) => $exception->getTrace())
->map(fn ($trace) => Arr::except($trace, ['args']))
->all(),
],

View File

@@ -1,7 +0,0 @@
<?php
namespace App\Exceptions\Service\Helper;
class CdnVersionFetchingException extends \Exception
{
}

View File

@@ -11,6 +11,7 @@ class ApiKeyResource extends Resource
protected static ?string $model = ApiKey::class;
protected static ?string $label = 'API Key';
protected static ?string $navigationIcon = 'tabler-key';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
{

View File

@@ -4,9 +4,13 @@ namespace App\Filament\Resources\ApiKeyResource\Pages;
use App\Filament\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
use Filament\Forms;
class CreateApiKey extends CreateRecord
{
@@ -18,26 +22,26 @@ class CreateApiKey extends CreateRecord
{
return $form
->schema([
Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
Forms\Components\Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
Forms\Components\Hidden::make('user_id')
Hidden::make('user_id')
->default(auth()->user()->id)
->required(),
Forms\Components\Hidden::make('key_type')
Hidden::make('key_type')
->inlineLabel()
->default(ApiKey::TYPE_APPLICATION)
->required(),
Forms\Components\Fieldset::make('Permissions')
Fieldset::make('Permissions')
->columns([
'default' => 1,
'sm' => 1,
'md' => 2,
])
->schema(
collect(ApiKey::RESOURCES)->map(fn ($resource) => Forms\Components\ToggleButtons::make("r_$resource")
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
->label(str($resource)->replace('_', ' ')->title())->inline()
->options([
0 => 'None',
@@ -67,15 +71,13 @@ class CreateApiKey extends CreateRecord
)->all(),
),
Forms\Components\TagsInput::make('allowed_ips')
TagsInput::make('allowed_ips')
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
->label('Whitelisted IPv4 Addresses')
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
->columnSpanFull()
->hidden()
->default(null),
->columnSpanFull(),
Forms\Components\Textarea::make('memo')
Textarea::make('memo')
->required()
->label('Description')
->helperText('

View File

@@ -6,8 +6,9 @@ use App\Filament\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Tables;
class ListApiKeys extends ListRecords
{
@@ -19,37 +20,37 @@ class ListApiKeys extends ListRecords
->searchable(false)
->modifyQueryUsing(fn ($query) => $query->where('key_type', ApiKey::TYPE_APPLICATION))
->columns([
Tables\Columns\TextColumn::make('key')
TextColumn::make('key')
->copyable()
->icon('tabler-clipboard-text')
->state(fn (ApiKey $key) => $key->identifier . $key->token),
Tables\Columns\TextColumn::make('memo')
TextColumn::make('memo')
->label('Description')
->wrap()
->limit(50),
Tables\Columns\TextColumn::make('identifier')
TextColumn::make('identifier')
->hidden()
->searchable(),
Tables\Columns\TextColumn::make('last_used_at')
TextColumn::make('last_used_at')
->label('Last Used')
->placeholder('Not Used')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
TextColumn::make('created_at')
->label('Created')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('user.username')
TextColumn::make('user.username')
->label('Created By')
->url(fn (ApiKey $apiKey): string => route('filament.admin.resources.users.edit', ['record' => $apiKey->user])),
])
->actions([
Tables\Actions\DeleteAction::make(),
DeleteAction::make(),
]);
}

View File

@@ -13,6 +13,7 @@ class DatabaseHostResource extends Resource
protected static ?string $label = 'Databases';
protected static ?string $navigationIcon = 'tabler-database';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
{

View File

@@ -3,10 +3,16 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\CreateRecord;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Model;
use PDOException;
class CreateDatabaseHost extends CreateRecord
{
@@ -30,14 +36,14 @@ class CreateDatabaseHost extends CreateRecord
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('host')
TextInput::make('host')
->columnSpan(2)
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
->required()
->live(onBlur: true)
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
->maxLength(191),
Forms\Components\TextInput::make('port')
->maxLength(255),
TextInput::make('port')
->columnSpan(1)
->helperText('The port that MySQL is running on for this host.')
->required()
@@ -45,26 +51,26 @@ class CreateDatabaseHost extends CreateRecord
->default(3306)
->minValue(0)
->maxValue(65535),
Forms\Components\TextInput::make('max_databases')
TextInput::make('max_databases')
->label('Max databases')
->helpertext('Blank is unlimited.')
->numeric(),
Forms\Components\TextInput::make('name')
TextInput::make('name')
->label('Display Name')
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
->required()
->maxLength(60),
Forms\Components\TextInput::make('username')
TextInput::make('username')
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
->required()
->maxLength(191),
Forms\Components\TextInput::make('password')
->maxLength(255),
TextInput::make('password')
->helperText('The password for the database user.')
->password()
->revealable()
->maxLength(191)
->maxLength(255)
->required(),
Forms\Components\Select::make('node_id')
Select::make('node_id')
->searchable()
->preload()
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
@@ -79,11 +85,30 @@ class CreateDatabaseHost extends CreateRecord
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
protected function handleRecordCreation(array $data): Model
{
return resolve(HostCreationService::class)->handle($data);
}
public function exception($e, $stopPropagation): void
{
if ($e instanceof PDOException) {
Notification::make()
->title('Error connecting to database host')
->body($e->getMessage())
->color('danger')
->icon('tabler-database')
->danger()
->send();
$stopPropagation();
}
}
}

View File

@@ -3,12 +3,19 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
use App\Models\DatabaseHost;
use App\Services\Databases\Hosts\HostUpdateService;
use Filament\Actions;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Model;
use PDOException;
class EditDatabaseHost extends EditRecord
{
@@ -26,40 +33,40 @@ class EditDatabaseHost extends EditRecord
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('host')
TextInput::make('host')
->columnSpan(2)
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
->required()
->live(onBlur: true)
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
->maxLength(191),
Forms\Components\TextInput::make('port')
->maxLength(255),
TextInput::make('port')
->columnSpan(1)
->helperText('The port that MySQL is running on for this host.')
->required()
->numeric()
->minValue(0)
->maxValue(65535),
Forms\Components\TextInput::make('max_databases')
TextInput::make('max_databases')
->label('Max databases')
->helpertext('Blank is unlimited.')
->numeric(),
Forms\Components\TextInput::make('name')
TextInput::make('name')
->label('Display Name')
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
->required()
->maxLength(60),
Forms\Components\TextInput::make('username')
TextInput::make('username')
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
->required()
->maxLength(191),
Forms\Components\TextInput::make('password')
->maxLength(255),
TextInput::make('password')
->helperText('The password for the database user.')
->password()
->revealable()
->maxLength(191)
->maxLength(255)
->required(),
Forms\Components\Select::make('node_id')
Select::make('node_id')
->searchable()
->preload()
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
@@ -87,7 +94,27 @@ class EditDatabaseHost extends EditRecord
public function getRelationManagers(): array
{
return [
DatabaseHostResource\RelationManagers\DatabasesRelationManager::class,
DatabasesRelationManager::class,
];
}
protected function handleRecordUpdate($record, array $data): Model
{
return resolve(HostUpdateService::class)->handle($record->id, $data);
}
public function exception($e, $stopPropagation): void
{
if ($e instanceof PDOException) {
Notification::make()
->title('Error connecting to database host')
->body($e->getMessage())
->color('danger')
->icon('tabler-database')
->danger()
->send();
$stopPropagation();
}
}
}

View File

@@ -5,7 +5,10 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ListDatabaseHosts extends ListRecords
@@ -19,30 +22,27 @@ class ListDatabaseHosts extends ListRecords
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('host')
TextColumn::make('host')
->searchable(),
Tables\Columns\TextColumn::make('port')
TextColumn::make('port')
->sortable(),
Tables\Columns\TextColumn::make('username')
TextColumn::make('username')
->searchable(),
Tables\Columns\TextColumn::make('max_databases')
TextColumn::make('max_databases')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('node.name')
TextColumn::make('node.name')
->numeric()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}

View File

@@ -4,11 +4,14 @@ namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
use App\Models\Database;
use App\Services\Databases\DatabasePasswordService;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class DatabasesRelationManager extends RelationManager
@@ -19,9 +22,9 @@ class DatabasesRelationManager extends RelationManager
{
return $form
->schema([
Forms\Components\TextInput::make('database')->columnSpanFull(),
Forms\Components\TextInput::make('username'),
Forms\Components\TextInput::make('password')
TextInput::make('database')->columnSpanFull(),
TextInput::make('username'),
TextInput::make('password')
->hintAction(
Action::make('rotate')
->icon('tabler-refresh')
@@ -29,12 +32,12 @@ class DatabasesRelationManager extends RelationManager
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
)
->formatStateUsing(fn (Database $database) => $database->password),
Forms\Components\TextInput::make('remote')->label('Connections From'),
Forms\Components\TextInput::make('max_connections'),
Forms\Components\TextInput::make('JDBC')
TextInput::make('remote')->label('Connections From'),
TextInput::make('max_connections'),
TextInput::make('JDBC')
->label('JDBC Connection String')
->columnSpanFull()
->formatStateUsing(fn (Forms\Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
]);
}
public function table(Table $table): Table
@@ -42,20 +45,18 @@ class DatabasesRelationManager extends RelationManager
return $table
->recordTitleAttribute('servers')
->columns([
Tables\Columns\TextColumn::make('database')->icon('tabler-database'),
Tables\Columns\TextColumn::make('username')->icon('tabler-user'),
//Tables\Columns\TextColumn::make('password'),
Tables\Columns\TextColumn::make('remote'),
Tables\Columns\TextColumn::make('server.name')
TextColumn::make('database')->icon('tabler-database'),
TextColumn::make('username')->icon('tabler-user'),
TextColumn::make('remote'),
TextColumn::make('server.name')
->icon('tabler-brand-docker')
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
Tables\Columns\TextColumn::make('max_connections'),
Tables\Columns\TextColumn::make('created_at')->dateTime(),
TextColumn::make('max_connections'),
TextColumn::make('created_at')->dateTime(),
])
->actions([
Tables\Actions\DeleteAction::make(),
Tables\Actions\ViewAction::make()->color('primary'),
//Tables\Actions\EditAction::make(),
DeleteAction::make(),
ViewAction::make()->color('primary'),
]);
}

View File

@@ -13,6 +13,7 @@ class DatabaseResource extends Resource
protected static ?string $navigationIcon = 'tabler-database';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
{

View File

@@ -3,9 +3,10 @@
namespace App\Filament\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
use Filament\Forms;
class CreateDatabase extends CreateRecord
{
@@ -15,29 +16,29 @@ class CreateDatabase extends CreateRecord
{
return $form
->schema([
Forms\Components\Select::make('server_id')
Select::make('server_id')
->relationship('server', 'name')
->searchable()
->preload()
->required(),
Forms\Components\TextInput::make('database_host_id')
TextInput::make('database_host_id')
->required()
->numeric(),
Forms\Components\TextInput::make('database')
TextInput::make('database')
->required()
->maxLength(191),
Forms\Components\TextInput::make('remote')
->maxLength(255),
TextInput::make('remote')
->required()
->maxLength(191)
->maxLength(255)
->default('%'),
Forms\Components\TextInput::make('username')
TextInput::make('username')
->required()
->maxLength(191),
Forms\Components\TextInput::make('password')
->maxLength(255),
TextInput::make('password')
->password()
->revealable()
->required(),
Forms\Components\TextInput::make('max_connections')
TextInput::make('max_connections')
->numeric()
->minValue(0)
->default(0),

View File

@@ -4,9 +4,10 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use Filament\Actions;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms;
class EditDatabase extends EditRecord
{
@@ -16,29 +17,29 @@ class EditDatabase extends EditRecord
{
return $form
->schema([
Forms\Components\Select::make('server_id')
Select::make('server_id')
->relationship('server', 'name')
->searchable()
->preload()
->required(),
Forms\Components\TextInput::make('database_host_id')
TextInput::make('database_host_id')
->required()
->numeric(),
Forms\Components\TextInput::make('database')
TextInput::make('database')
->required()
->maxLength(191),
Forms\Components\TextInput::make('remote')
->maxLength(255),
TextInput::make('remote')
->required()
->maxLength(191)
->maxLength(255)
->default('%'),
Forms\Components\TextInput::make('username')
TextInput::make('username')
->required()
->maxLength(191),
Forms\Components\TextInput::make('password')
->maxLength(255),
TextInput::make('password')
->password()
->revealable()
->required(),
Forms\Components\TextInput::make('max_connections')
TextInput::make('max_connections')
->numeric()
->minValue(0)
->default(0),

View File

@@ -4,9 +4,12 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use Filament\Actions;
use Filament\Tables\Actions\EditAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Tables;
class ListDatabases extends ListRecords
{
@@ -16,39 +19,36 @@ class ListDatabases extends ListRecords
{
return $table
->columns([
Tables\Columns\TextColumn::make('server.name')
TextColumn::make('server.name')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('database_host_id')
TextColumn::make('database_host_id')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('database')
TextColumn::make('database')
->searchable(),
Tables\Columns\TextColumn::make('username')
TextColumn::make('username')
->searchable(),
Tables\Columns\TextColumn::make('remote')
TextColumn::make('remote')
->searchable(),
Tables\Columns\TextColumn::make('max_connections')
TextColumn::make('max_connections')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}

View File

@@ -3,6 +3,18 @@
namespace App\Filament\Resources\EggResource\Pages;
use App\Filament\Resources\EggResource;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Pages\CreateRecord;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use Filament\Forms;
@@ -19,26 +31,26 @@ class CreateEgg extends CreateRecord
{
return $form
->schema([
Forms\Components\Tabs::make()->tabs([
Forms\Components\Tabs\Tab::make('Configuration')
Tabs::make()->tabs([
Tab::make('Configuration')
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
Forms\Components\TextInput::make('name')
TextInput::make('name')
->required()
->maxLength(191)
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
Forms\Components\TextInput::make('author')
->maxLength(191)
TextInput::make('author')
->maxLength(255)
->required()
->email()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('The author of this version of the Egg.'),
Forms\Components\Textarea::make('description')
Textarea::make('description')
->rows(3)
->columnSpanFull()
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
Forms\Components\Textarea::make('startup')
Textarea::make('startup')
->rows(3)
->columnSpanFull()
->required()
@@ -46,26 +58,26 @@ class CreateEgg extends CreateRecord
'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}',
]))
->helperText('The default startup command that should be used for new servers using this Egg.'),
Forms\Components\TagsInput::make('features')
TagsInput::make('features')
->placeholder('Add Feature')
->helperText('')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\Toggle::make('force_outgoing_ip')
Toggle::make('force_outgoing_ip')
->hintIcon('tabler-question-mark')
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
Required for certain games to work properly when the Node has multiple public IP addresses.
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
Forms\Components\Hidden::make('script_is_privileged')
Hidden::make('script_is_privileged')
->default(1),
Forms\Components\TagsInput::make('tags')
TagsInput::make('tags')
->placeholder('Add Tags')
->helperText('')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\TextInput::make('update_url')
TextInput::make('update_url')
->disabled()
->helperText('Not implemented.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\KeyValue::make('docker_images')
KeyValue::make('docker_images')
->live()
->columnSpanFull()
->required()
@@ -77,37 +89,37 @@ class CreateEgg extends CreateRecord
->helperText('The docker images available to servers using this egg.'),
]),
Forms\Components\Tabs\Tab::make('Process Management')
Tab::make('Process Management')
->columns()
->schema([
Forms\Components\Hidden::make('config_from')
Hidden::make('config_from')
->default(null)
->label('Copy Settings From')
// ->placeholder('None')
// ->relationship('configFrom', 'name', ignoreRecord: true)
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
Forms\Components\TextInput::make('config_stop')
TextInput::make('config_stop')
->required()
->maxLength(191)
->maxLength(255)
->label('Stop Command')
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
Textarea::make('config_startup')->rows(10)->json()
->label('Start Configuration')
->default('{}')
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
Forms\Components\Textarea::make('config_files')->rows(10)->json()
Textarea::make('config_files')->rows(10)->json()
->label('Configuration Files')
->default('{}')
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
Textarea::make('config_logs')->rows(10)->json()
->label('Log Configuration')
->default('{}')
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
]),
Forms\Components\Tabs\Tab::make('Egg Variables')
Tab::make('Egg Variables')
->columnSpanFull()
->schema([
Forms\Components\Repeater::make('variables')
Repeater::make('variables')
->label('')
->addActionLabel('Add New Egg Variable')
->grid()
@@ -137,46 +149,46 @@ class CreateEgg extends CreateRecord
return $data;
})
->schema([
Forms\Components\TextInput::make('name')
TextInput::make('name')
->live()
->debounce(750)
->maxLength(191)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
)
->required(),
Forms\Components\Textarea::make('description')->columnSpanFull(),
Forms\Components\TextInput::make('env_variable')
Textarea::make('description')->columnSpanFull(),
TextInput::make('env_variable')
->label('Environment Variable')
->maxLength(191)
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code')
->hintIconTooltip(fn ($state) => "{{{$state}}}")
->required(),
Forms\Components\TextInput::make('default_value')->maxLength(191),
Forms\Components\Fieldset::make('User Permissions')
TextInput::make('default_value')->maxLength(255),
Fieldset::make('User Permissions')
->schema([
Forms\Components\Checkbox::make('user_viewable')->label('Viewable'),
Forms\Components\Checkbox::make('user_editable')->label('Editable'),
Checkbox::make('user_viewable')->label('Viewable'),
Checkbox::make('user_editable')->label('Editable'),
]),
Forms\Components\Textarea::make('rules')->columnSpanFull(),
Textarea::make('rules')->columnSpanFull(),
]),
]),
Forms\Components\Tabs\Tab::make('Install Script')
Tab::make('Install Script')
->columns(3)
->schema([
Forms\Components\Hidden::make('copy_script_from'),
Hidden::make('copy_script_from'),
//->placeholder('None')
//->relationship('scriptFrom', 'name', ignoreRecord: true),
Forms\Components\TextInput::make('script_container')
TextInput::make('script_container')
->required()
->maxLength(191)
->maxLength(255)
->default('alpine:3.4'),
Forms\Components\Select::make('script_entry')
Select::make('script_entry')
->selectablePlaceholder(false)
->default('bash')
->options(['bash', 'ash', '/bin/bash'])

View File

@@ -3,10 +3,29 @@
namespace App\Filament\Resources\EggResource\Pages;
use App\Filament\Resources\EggResource;
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Services\Eggs\Sharing\EggExporterService;
use Filament\Forms;
use Filament\Forms\Form;
@@ -18,64 +37,64 @@ class EditEgg extends EditRecord
{
return $form
->schema([
Forms\Components\Tabs::make()->tabs([
Forms\Components\Tabs\Tab::make('Configuration')
Tabs::make()->tabs([
Tab::make('Configuration')
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
Forms\Components\TextInput::make('name')
TextInput::make('name')
->required()
->maxLength(191)
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
Forms\Components\TextInput::make('uuid')
TextInput::make('uuid')
->label('Egg UUID')
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'),
Forms\Components\TextInput::make('id')
TextInput::make('id')
->label('Egg ID')
->disabled(),
Forms\Components\Textarea::make('description')
Textarea::make('description')
->rows(3)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
Forms\Components\TextInput::make('author')
TextInput::make('author')
->required()
->maxLength(191)
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'),
Forms\Components\Textarea::make('startup')
Textarea::make('startup')
->rows(2)
->columnSpanFull()
->required()
->helperText('The default startup command that should be used for new servers using this Egg.'),
Forms\Components\TagsInput::make('file_denylist')
TagsInput::make('file_denylist')
->hidden() // latest wings breaks it.
->placeholder('denied-file.txt')
->helperText('A list of files that the end user is not allowed to edit.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\TagsInput::make('features')
TagsInput::make('features')
->placeholder('Add Feature')
->helperText('')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\Toggle::make('force_outgoing_ip')
Toggle::make('force_outgoing_ip')
->hintIcon('tabler-question-mark')
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
Required for certain games to work properly when the Node has multiple public IP addresses.
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
Forms\Components\Hidden::make('script_is_privileged')
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
Forms\Components\TagsInput::make('tags')
TagsInput::make('tags')
->placeholder('Add Tags')
->helperText('')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\TextInput::make('update_url')
TextInput::make('update_url')
->disabled()
->helperText('Not implemented.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\KeyValue::make('docker_images')
KeyValue::make('docker_images')
->live()
->columnSpanFull()
->required()
@@ -85,32 +104,32 @@ class EditEgg extends EditRecord
->helperText('The docker images available to servers using this egg.'),
]),
Forms\Components\Tabs\Tab::make('Process Management')
Tab::make('Process Management')
->columns()
->schema([
Forms\Components\Select::make('config_from')
Select::make('config_from')
->label('Copy Settings From')
->placeholder('None')
->relationship('configFrom', 'name', ignoreRecord: true)
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
Forms\Components\TextInput::make('config_stop')
->maxLength(191)
TextInput::make('config_stop')
->maxLength(255)
->label('Stop Command')
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
Textarea::make('config_startup')->rows(10)->json()
->label('Start Configuration')
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
Forms\Components\Textarea::make('config_files')->rows(10)->json()
Textarea::make('config_files')->rows(10)->json()
->label('Configuration Files')
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
Textarea::make('config_logs')->rows(10)->json()
->label('Log Configuration')
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
]),
Forms\Components\Tabs\Tab::make('Egg Variables')
Tab::make('Egg Variables')
->columnSpanFull()
->schema([
Forms\Components\Repeater::make('variables')
Repeater::make('variables')
->label('')
->grid()
->relationship('variables')
@@ -139,48 +158,48 @@ class EditEgg extends EditRecord
return $data;
})
->schema([
Forms\Components\TextInput::make('name')
TextInput::make('name')
->live()
->debounce(750)
->maxLength(191)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
)
->required(),
Forms\Components\Textarea::make('description')->columnSpanFull(),
Forms\Components\TextInput::make('env_variable')
Textarea::make('description')->columnSpanFull(),
TextInput::make('env_variable')
->label('Environment Variable')
->maxLength(191)
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code')
->hintIconTooltip(fn ($state) => "{{{$state}}}")
->required(),
Forms\Components\TextInput::make('default_value')->maxLength(191),
Forms\Components\Fieldset::make('User Permissions')
TextInput::make('default_value')->maxLength(255),
Fieldset::make('User Permissions')
->schema([
Forms\Components\Checkbox::make('user_viewable')->label('Viewable'),
Forms\Components\Checkbox::make('user_editable')->label('Editable'),
Checkbox::make('user_viewable')->label('Viewable'),
Checkbox::make('user_editable')->label('Editable'),
]),
Forms\Components\TextInput::make('rules')->columnSpanFull(),
TextInput::make('rules')->columnSpanFull(),
]),
]),
Forms\Components\Tabs\Tab::make('Install Script')
Tab::make('Install Script')
->columns(3)
->schema([
Forms\Components\Select::make('copy_script_from')
Select::make('copy_script_from')
->placeholder('None')
->relationship('scriptFrom', 'name', ignoreRecord: true),
Forms\Components\TextInput::make('script_container')
TextInput::make('script_container')
->required()
->maxLength(191)
->maxLength(255)
->default('alpine:3.4'),
Forms\Components\TextInput::make('script_entry')
TextInput::make('script_entry')
->required()
->maxLength(191)
->maxLength(255)
->default('ash'),
MonacoEditor::make('script_install')
@@ -198,19 +217,97 @@ class EditEgg extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
Actions\DeleteAction::make('deleteEgg')
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete Egg' : 'Egg In Use'),
Actions\ExportAction::make()
->icon('tabler-download')
->label('Export Egg')
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
Actions\Action::make('exportEgg')
->label('Export')
->color('primary')
// TODO uses old admin panel export service
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json')),
Actions\Action::make('importEgg')
->label('Import')
->form([
Placeholder::make('warning')
->label('This will overwrite the current egg to the one you upload.'),
Tabs::make('Tabs')
->tabs([
Tab::make('From File')
->icon('tabler-file-upload')
->schema([
FileUpload::make('egg')
->label('Egg')
->hint('eg. minecraft.json')
->acceptedFileTypes(['application/json'])
->storeFiles(false),
]),
Tab::make('From URL')
->icon('tabler-world-upload')
->schema([
TextInput::make('url')
->label('URL')
->hint('Link to the egg file (eg. minecraft.json)')
->url(),
]),
])
->contained(false),
])
->action(function (array $data, Egg $egg): void {
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
if (!empty($data['egg'])) {
try {
$eggImportService->fromFile($data['egg'], $egg);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->send();
report($exception);
return;
}
}
if (!empty($data['url'])) {
try {
$eggImportService->fromUrl($data['url'], $egg);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->send();
report($exception);
return;
}
}
$this->refreshForm();
Notification::make()
->title('Import Success')
->success()
->send();
}),
$this->getSaveFormAction()->formId('form'),
];
}
public function refreshForm(): void
{
$this->fillForm();
}
protected function getFormActions(): array
{
return [];
@@ -219,7 +316,7 @@ class EditEgg extends EditRecord
public function getRelationManagers(): array
{
return [
EggResource\RelationManagers\ServersRelationManager::class,
ServersRelationManager::class,
];
}
}

View File

@@ -4,13 +4,20 @@ namespace App\Filament\Resources\EggResource\Pages;
use App\Filament\Resources\EggResource;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Filament\Tables;
@@ -22,36 +29,37 @@ class ListEggs extends ListRecords
public function table(Table $table): Table
{
return $table
->searchable(false)
->searchable(true)
->defaultPaginationPageOption(25)
->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0)
->columns([
Tables\Columns\TextColumn::make('id')
TextColumn::make('id')
->label('Id')
->hidden()
->searchable(),
Tables\Columns\TextColumn::make('name')
->hidden(),
TextColumn::make('name')
->icon('tabler-egg')
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
->wrap()
->searchable(),
Tables\Columns\TextColumn::make('servers_count')
->searchable()
->sortable(),
TextColumn::make('servers_count')
->counts('servers')
->icon('tabler-server')
->label('Servers'),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\ExportAction::make()
EditAction::make(),
Tables\Actions\Action::make('export')
->icon('tabler-download')
->label('Export')
->color('primary')
// TODO uses old admin panel export service
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg])),
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json')),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
@@ -65,20 +73,20 @@ class ListEggs extends ListRecords
->form([
Tabs::make('Tabs')
->tabs([
Tabs\Tab::make('From File')
Tab::make('From File')
->icon('tabler-file-upload')
->schema([
Forms\Components\FileUpload::make('egg')
FileUpload::make('egg')
->label('Egg')
->hint('This should be the json file ( egg-minecraft.json )')
->acceptedFileTypes(['application/json'])
->storeFiles(false)
->multiple(),
]),
Tabs\Tab::make('From URL')
Tab::make('From URL')
->icon('tabler-world-upload')
->schema([
Forms\Components\TextInput::make('url')
TextInput::make('url')
->label('URL')
->hint('This URL should point to a single json file')
->url(),
@@ -88,7 +96,6 @@ class ListEggs extends ListRecords
])
->action(function (array $data): void {
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);

View File

@@ -4,7 +4,8 @@ namespace App\Filament\Resources\EggResource\RelationManagers;
use App\Models\Server;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ServersRelationManager extends RelationManager
@@ -18,23 +19,23 @@ class ServersRelationManager extends RelationManager
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('user.username')
TextColumn::make('user.username')
->label('Owner')
->icon('tabler-user')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->sortable(),
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->icon('tabler-brand-docker')
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
->sortable(),
Tables\Columns\TextColumn::make('node.name')
TextColumn::make('node.name')
->icon('tabler-server-2')
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
Tables\Columns\TextColumn::make('image')
TextColumn::make('image')
->label('Docker Image'),
Tables\Columns\SelectColumn::make('allocation.id')
SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
]);

View File

@@ -11,6 +11,7 @@ class MountResource extends Resource
protected static ?string $model = Mount::class;
protected static ?string $navigationIcon = 'tabler-layers-linked';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
{

View File

@@ -4,11 +4,14 @@ namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
use Filament\Forms;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@@ -23,11 +26,11 @@ class CreateMount extends CreateRecord
return $form
->schema([
Section::make()->schema([
Forms\Components\TextInput::make('name')
TextInput::make('name')
->required()
->helperText('Unique name used to separate this mount from another.')
->maxLength(64),
Forms\Components\ToggleButtons::make('read_only')
ToggleButtons::make('read_only')
->label('Read only?')
->helperText('Is the mount read only inside the container?')
->options([
@@ -45,15 +48,15 @@ class CreateMount extends CreateRecord
->inline()
->default(false)
->required(),
Forms\Components\TextInput::make('source')
TextInput::make('source')
->required()
->helperText('File path on the host system to mount to a container.')
->maxLength(191),
Forms\Components\TextInput::make('target')
->maxLength(255),
TextInput::make('target')
->required()
->helperText('Where the mount will be accessible inside a container.')
->maxLength(191),
Forms\Components\ToggleButtons::make('user_mountable')
->maxLength(255),
ToggleButtons::make('user_mountable')
->hidden()
->label('User mountable?')
->options([
@@ -71,10 +74,10 @@ class CreateMount extends CreateRecord
->default(false)
->inline()
->required(),
Forms\Components\Textarea::make('description')
Textarea::make('description')
->helperText('A longer description for this mount.')
->columnSpanFull(),
Forms\Components\Hidden::make('user_mountable')->default(1),
Hidden::make('user_mountable')->default(1),
])->columnSpan(1)->columns([
'default' => 1,
'lg' => 2,

View File

@@ -4,8 +4,10 @@ namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use Filament\Actions;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
@@ -20,11 +22,11 @@ class EditMount extends EditRecord
return $form
->schema([
Section::make()->schema([
Forms\Components\TextInput::make('name')
TextInput::make('name')
->required()
->helperText('Unique name used to separate this mount from another.')
->maxLength(64),
Forms\Components\ToggleButtons::make('read_only')
ToggleButtons::make('read_only')
->label('Read only?')
->helperText('Is the mount read only inside the container?')
->options([
@@ -42,15 +44,15 @@ class EditMount extends EditRecord
->inline()
->default(false)
->required(),
Forms\Components\TextInput::make('source')
TextInput::make('source')
->required()
->helperText('File path on the host system to mount to a container.')
->maxLength(191),
Forms\Components\TextInput::make('target')
->maxLength(255),
TextInput::make('target')
->required()
->helperText('Where the mount will be accessible inside a container.')
->maxLength(191),
Forms\Components\ToggleButtons::make('user_mountable')
->maxLength(255),
ToggleButtons::make('user_mountable')
->hidden()
->label('User mountable?')
->options([
@@ -68,7 +70,7 @@ class EditMount extends EditRecord
->default(false)
->inline()
->required(),
Forms\Components\Textarea::make('description')
Textarea::make('description')
->helperText('A longer description for this mount.')
->columnSpanFull(),
])->columnSpan(1)->columns([

View File

@@ -6,9 +6,13 @@ use App\Filament\Resources\MountResource;
use App\Models\Mount;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Tables;
class ListMounts extends ListRecords
{
@@ -18,31 +22,28 @@ class ListMounts extends ListRecords
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('source')
TextColumn::make('source')
->searchable(),
Tables\Columns\TextColumn::make('target')
TextColumn::make('target')
->searchable(),
Tables\Columns\IconColumn::make('read_only')
IconColumn::make('read_only')
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
->color(fn (bool $state) => $state ? 'success' : 'danger')
->sortable(),
Tables\Columns\IconColumn::make('user_mountable')
IconColumn::make('user_mountable')
->hidden()
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
->color(fn (bool $state) => $state ? 'success' : 'danger')
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->emptyStateIcon('tabler-layers-linked')

View File

@@ -3,7 +3,8 @@
namespace App\Filament\Resources;
use App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource\RelationManagers;
use App\Filament\Resources\NodeResource\RelationManagers\AllocationsRelationManager;
use App\Filament\Resources\NodeResource\RelationManagers\NodesRelationManager;
use App\Models\Node;
use Filament\Resources\Resource;
@@ -23,8 +24,8 @@ class NodeResource extends Resource
public static function getRelations(): array
{
return [
RelationManagers\AllocationsRelationManager::class,
RelationManagers\NodesRelationManager::class,
AllocationsRelationManager::class,
NodesRelationManager::class,
];
}

View File

@@ -3,9 +3,18 @@
namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class CreateNode extends CreateRecord
@@ -18,21 +27,21 @@ class CreateNode extends CreateRecord
public function form(Forms\Form $form): Forms\Form
{
return $form->schema([
Tabs::make('Tabs')
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Basic Settings')
return $form
->schema([
Wizard::make([
Step::make('basic')
->label('Basic Settings')
->icon('tabler-server')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('fqdn')
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
@@ -55,7 +64,7 @@ class CreateNode extends CreateRecord
return "
This is the domain name that points to your node's IP Address.
If you've already set up this, you can verify it by checking the next field!
";
";
})
->hintColor('danger')
->hint(function ($state) {
@@ -65,7 +74,7 @@ class CreateNode extends CreateRecord
return '';
})
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
@@ -91,19 +100,19 @@ class CreateNode extends CreateRecord
$set('dns', false);
})
->maxLength(191),
->maxLength(255),
Forms\Components\TextInput::make('ip')
TextInput::make('ip')
->disabled()
->hidden(),
Forms\Components\ToggleButtons::make('dns')
ToggleButtons::make('dns')
->label('DNS Record Check')
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
->disabled()
->inline()
->default(null)
->hint(fn (Forms\Get $get) => $get('ip'))
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => 'Valid',
@@ -120,7 +129,7 @@ class CreateNode extends CreateRecord
'lg' => 1,
]),
Forms\Components\TextInput::make('daemon_listen')
TextInput::make('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
@@ -129,13 +138,13 @@ class CreateNode extends CreateRecord
])
->label(trans('strings.port'))
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
->minValue(0)
->maxValue(65536)
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer(),
Forms\Components\TextInput::make('name')
TextInput::make('name')
->label('Display Name')
->columnSpan([
'default' => 1,
@@ -148,7 +157,7 @@ class CreateNode extends CreateRecord
->helperText('This name is for display only and can be changed later.')
->maxLength(100),
Forms\Components\ToggleButtons::make('scheme')
ToggleButtons::make('scheme')
->label('Communicate over SSL')
->columnSpan([
'default' => 1,
@@ -156,9 +165,8 @@ class CreateNode extends CreateRecord
'md' => 1,
'lg' => 1,
])
->required()
->inline()
->helperText(function (Forms\Get $get) {
->helperText(function (Get $get) {
if (request()->isSecure()) {
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
}
@@ -184,31 +192,18 @@ class CreateNode extends CreateRecord
])
->default(fn () => request()->isSecure() ? 'https' : 'http'),
]),
Tabs\Tab::make('Advanced Settings')
Step::make('advanced')
->label('Advanced Settings')
->icon('tabler-server-cog')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
Forms\Components\TextInput::make('upload_size')
->label('Upload Limit')
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\ToggleButtons::make('public')
->label('Automatic Allocation')->inline()
->default(true)
->columnSpan(1)
->options([
true => 'Yes',
false => 'No',
])
->colors([
true => 'success',
false => 'danger',
]),
Forms\Components\ToggleButtons::make('maintenance_mode')
ToggleButtons::make('maintenance_mode')
->label('Maintenance Mode')->inline()
->columnSpan(1)
->default(false)
@@ -222,22 +217,55 @@ class CreateNode extends CreateRecord
true => 'danger',
false => 'success',
]),
Forms\Components\TagsInput::make('tags')
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label('Automatic Allocation')->inline()
->options([
true => 'Yes',
false => 'No',
])
->colors([
true => 'success',
false => 'danger',
]),
TagsInput::make('tags')
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented')
->columnSpan(1),
Forms\Components\Grid::make()
->columnSpan(2),
TextInput::make('upload_size')
->label('Upload Limit')
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label('SFTP Port')
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(2)
->label('SFTP Alias')
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => 'Unlimited',
@@ -248,19 +276,20 @@ class CreateNode extends CreateRecord
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0),
Forms\Components\TextInput::make('memory_overallocate')
->default(0)
->required(),
TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label('Overallocate')->inlineLabel()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hidden(fn (Get $get) => $get('unlimited_mem'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
@@ -268,18 +297,19 @@ class CreateNode extends CreateRecord
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%'),
->suffix('%')
->required(),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
@@ -289,18 +319,19 @@ class CreateNode extends CreateRecord
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix('MiB')
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0),
Forms\Components\TextInput::make('disk_overallocate')
->default(0)
->required(),
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
@@ -309,18 +340,19 @@ class CreateNode extends CreateRecord
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%'),
->suffix('%')
->required(),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
@@ -330,18 +362,19 @@ class CreateNode extends CreateRecord
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0),
Forms\Components\TextInput::make('cpu_overallocate')
->minValue(0)
->required(),
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
@@ -350,11 +383,21 @@ class CreateNode extends CreateRecord
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%'),
->suffix('%')
->required(),
]),
]),
]),
]);
])->columnSpanFull()
->nextAction(fn (Action $action) => $action->label('Next Step'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
type="submit"
size="sm"
>
Create Node
</x-filament::button>
BLADE))),
]);
}
protected function getRedirectUrlParameters(): array
@@ -363,4 +406,9 @@ class CreateNode extends CreateRecord
'tab' => '-configuration-tab',
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@@ -9,7 +9,16 @@ use App\Models\Node;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\HtmlString;
@@ -32,10 +41,10 @@ class EditNode extends EditRecord
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Basic Settings')
Tab::make('Basic Settings')
->icon('tabler-server')
->schema([
Forms\Components\TextInput::make('fqdn')
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
@@ -68,7 +77,7 @@ class EditNode extends EditRecord
return '';
})
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
@@ -94,19 +103,19 @@ class EditNode extends EditRecord
$set('dns', false);
})
->maxLength(191),
->maxLength(255),
Forms\Components\TextInput::make('ip')
TextInput::make('ip')
->disabled()
->hidden(),
Forms\Components\ToggleButtons::make('dns')
ToggleButtons::make('dns')
->label('DNS Record Check')
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
->disabled()
->inline()
->default(null)
->hint(fn (Forms\Get $get) => $get('ip'))
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => 'Valid',
@@ -123,7 +132,7 @@ class EditNode extends EditRecord
'lg' => 1,
]),
Forms\Components\TextInput::make('daemon_listen')
TextInput::make('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
@@ -132,13 +141,13 @@ class EditNode extends EditRecord
])
->label(trans('strings.port'))
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
->minValue(0)
->maxValue(65536)
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer(),
Forms\Components\TextInput::make('name')
TextInput::make('name')
->label('Display Name')
->columnSpan([
'default' => 1,
@@ -151,7 +160,7 @@ class EditNode extends EditRecord
->helperText('This name is for display only and can be changed later.')
->maxLength(100),
Forms\Components\ToggleButtons::make('scheme')
ToggleButtons::make('scheme')
->label('Communicate over SSL')
->columnSpan([
'default' => 1,
@@ -159,9 +168,8 @@ class EditNode extends EditRecord
'md' => 1,
'lg' => 1,
])
->required()
->inline()
->helperText(function (Forms\Get $get) {
->helperText(function (Get $get) {
if (request()->isSecure()) {
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
}
@@ -186,27 +194,27 @@ class EditNode extends EditRecord
'https' => 'tabler-lock',
])
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
Tabs\Tab::make('Advanced Settings')
Tab::make('Advanced Settings')
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
->icon('tabler-server-cog')
->schema([
Forms\Components\TextInput::make('id')
TextInput::make('id')
->label('Node ID')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->disabled(),
Forms\Components\TextInput::make('uuid')
TextInput::make('uuid')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->label('Node UUID')
->hintAction(CopyAction::make())
->disabled(),
Forms\Components\TagsInput::make('tags')
TagsInput::make('tags')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented'),
Forms\Components\TextInput::make('upload_size')
TextInput::make('upload_size')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->label('Upload Limit')
->hintIcon('tabler-question-mark')
@@ -214,20 +222,20 @@ class EditNode extends EditRecord
->numeric()->required()
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\TextInput::make('daemon_sftp')
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Port')
->minValue(0)
->maxValue(65536)
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
Forms\Components\TextInput::make('daemon_sftp_alias')
TextInput::make('daemon_sftp_alias')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Alias')
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
Forms\Components\ToggleButtons::make('public')
ToggleButtons::make('public')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Automatic Allocation')->inline()
->options([
@@ -238,7 +246,7 @@ class EditNode extends EditRecord
true => 'success',
false => 'danger',
]),
Forms\Components\ToggleButtons::make('maintenance_mode')
ToggleButtons::make('maintenance_mode')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Maintenance Mode')->inline()
->hinticon('tabler-question-mark')
@@ -251,15 +259,15 @@ class EditNode extends EditRecord
false => 'success',
true => 'danger',
]),
Forms\Components\Grid::make()
Grid::make()
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => 'Unlimited',
@@ -270,20 +278,20 @@ class EditNode extends EditRecord
false => 'warning',
])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
Forms\Components\TextInput::make('memory')
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(0),
Forms\Components\TextInput::make('memory_overallocate')
TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label('Overallocate')->inlineLabel()
->required()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hidden(fn (Get $get) => $get('unlimited_mem'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
@@ -292,15 +300,15 @@ class EditNode extends EditRecord
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
@@ -310,18 +318,18 @@ class EditNode extends EditRecord
false => 'warning',
])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
Forms\Components\TextInput::make('disk')
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix('MiB')
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(0),
Forms\Components\TextInput::make('disk_overallocate')
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
@@ -332,16 +340,16 @@ class EditNode extends EditRecord
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
@@ -351,18 +359,18 @@ class EditNode extends EditRecord
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('cpu_overallocate')
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
@@ -374,15 +382,15 @@ class EditNode extends EditRecord
->suffix('%'),
]),
]),
Tabs\Tab::make('Configuration File')
Tab::make('Configuration File')
->icon('tabler-code')
->schema([
Forms\Components\Placeholder::make('instructions')
Placeholder::make('instructions')
->columnSpanFull()
->content(new HtmlString('
Save this file to your <span title="usually /etc/pelican/">daemon\'s root directory</span>, named <code>config.yml</code>
')),
Forms\Components\Textarea::make('config')
Textarea::make('config')
->label('/etc/pelican/config.yml')
->disabled()
->rows(19)
@@ -395,10 +403,11 @@ class EditNode extends EditRecord
->requiresConfirmation()
->modalHeading('Reset Daemon Token?')
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
->action(fn (NodeUpdateService $nodeUpdateService, Node $node) => $nodeUpdateService->handle($node, [], true)
&& Notification::make()->success()->title('Daemon Key Reset')->send()
&& $this->fillForm()
),
->action(function (NodeUpdateService $nodeUpdateService, Node $node) {
$nodeUpdateService->handle($node, [], true);
Notification::make()->success()->title('Daemon Key Reset')->send();
$this->fillForm();
}),
]),
]),
]),

View File

@@ -6,9 +6,13 @@ use App\Filament\Resources\NodeResource;
use App\Models\Node;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Tables;
class ListNodes extends ListRecords
{
@@ -20,70 +24,67 @@ class ListNodes extends ListRecords
->searchable(false)
->checkIfRecordIsSelectableUsing(fn (Node $node) => $node->servers_count <= 0)
->columns([
Tables\Columns\TextColumn::make('uuid')
TextColumn::make('uuid')
->label('UUID')
->searchable()
->hidden(),
Tables\Columns\IconColumn::make('health')
IconColumn::make('health')
->alignCenter()
->state(fn (Node $node) => $node)
->view('livewire.columns.version-column'),
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->icon('tabler-server-2')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('fqdn')
TextColumn::make('fqdn')
->visibleFrom('md')
->label('Address')
->icon('tabler-network')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('memory')
TextColumn::make('memory')
->visibleFrom('sm')
->icon('tabler-device-desktop-analytics')
->numeric()
->suffix(' GiB')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
->suffix(config('panel.use_binary_prefix') ? ' GiB' : ' GB')
->formatStateUsing(fn ($state) => number_format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), 2))
->sortable(),
Tables\Columns\TextColumn::make('disk')
TextColumn::make('disk')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(' GiB')
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
->suffix(config('panel.use_binary_prefix') ? ' GiB' : ' GB')
->formatStateUsing(fn ($state) => number_format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), 2))
->sortable(),
Tables\Columns\TextColumn::make('cpu')
TextColumn::make('cpu')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(' %')
->sortable(),
Tables\Columns\IconColumn::make('scheme')
IconColumn::make('scheme')
->visibleFrom('xl')
->label('SSL')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open-off')
->state(fn (Node $node) => $node->scheme === 'https'),
Tables\Columns\IconColumn::make('public')
IconColumn::make('public')
->visibleFrom('lg')
->trueIcon('tabler-eye-check')
->falseIcon('tabler-eye-cancel'),
Tables\Columns\TextColumn::make('servers_count')
TextColumn::make('servers_count')
->visibleFrom('sm')
->counts('servers')
->label('Servers')
->sortable()
->icon('tabler-brand-docker'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->emptyStateIcon('tabler-server-2')

View File

@@ -3,15 +3,24 @@
namespace App\Filament\Resources\NodeResource\RelationManagers;
use App\Models\Allocation;
use App\Models\Server;
use App\Models\Node;
use App\Services\Allocations\AssignmentService;
use Filament\Forms;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Set;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;
/**
* @method Node getOwnerRecord()
*/
class AllocationsRelationManager extends RelationManager
{
protected static string $relationship = 'allocations';
@@ -22,7 +31,7 @@ class AllocationsRelationManager extends RelationManager
{
return $form
->schema([
Forms\Components\TextInput::make('ip')
TextInput::make('ip')
->required()
->maxLength(255),
]);
@@ -40,19 +49,19 @@ class AllocationsRelationManager extends RelationManager
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->searchable()
->columns([
Tables\Columns\TextColumn::make('id'),
Tables\Columns\TextColumn::make('port')
TextColumn::make('id'),
TextColumn::make('port')
->searchable()
->label('Port'),
Tables\Columns\TextColumn::make('server.name')
TextColumn::make('server.name')
->label('Server')
->icon('tabler-brand-docker')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
Tables\Columns\TextInputColumn::make('ip_alias')
TextInputColumn::make('ip_alias')
->searchable()
->label('Alias'),
Tables\Columns\TextInputColumn::make('ip')
TextInputColumn::make('ip')
->searchable()
->label('IP'),
])
@@ -65,20 +74,20 @@ class AllocationsRelationManager extends RelationManager
->headerActions([
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
->form(fn () => [
Forms\Components\TextInput::make('allocation_ip')
->datalist($this->getOwnerRecord()->ipAddresses() ?? [])
TextInput::make('allocation_ip')
->datalist($this->getOwnerRecord()->ipAddresses())
->label('IP Address')
->inlineLabel()
->ipv4()
->helperText("Usually your machine's public IP unless you are port forwarding.")
->required(),
Forms\Components\TextInput::make('allocation_alias')
TextInput::make('allocation_alias')
->label('Alias')
->inlineLabel()
->default(null)
->helperText('Optional display name to help you remember what these are.')
->required(false),
Forms\Components\TagsInput::make('allocation_ports')
TagsInput::make('allocation_ports')
->placeholder('Examples: 27015, 27017-27019')
->helperText(new HtmlString('
These are the ports that users can connect to this Server through.
@@ -88,7 +97,7 @@ class AllocationsRelationManager extends RelationManager
->label('Ports')
->inlineLabel()
->live()
->afterStateUpdated(function ($state, Forms\Set $set) {
->afterStateUpdated(function ($state, Set $set) {
$ports = collect();
$update = false;
foreach ($state as $portEntry) {
@@ -142,9 +151,8 @@ class AllocationsRelationManager extends RelationManager
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord(), $data)),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
// Tables\Actions\DissociateBulkAction::make(),
Tables\Actions\DeleteBulkAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}

View File

@@ -3,7 +3,8 @@
namespace App\Filament\Resources\NodeResource\RelationManagers;
use App\Models\Server;
use Filament\Tables;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Resources\RelationManagers\RelationManager;
@@ -18,34 +19,34 @@ class NodesRelationManager extends RelationManager
return $table
->searchable(false)
->columns([
Tables\Columns\TextColumn::make('user.username')
TextColumn::make('user.username')
->label('Owner')
->icon('tabler-user')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->searchable(),
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->icon('tabler-brand-docker')
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('egg.name')
TextColumn::make('egg.name')
->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user]))
->sortable(),
Tables\Columns\SelectColumn::make('allocation.id')
SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
Tables\Columns\TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
Tables\Columns\TextColumn::make('cpu')->icon('tabler-cpu'),
Tables\Columns\TextColumn::make('databases_count')
TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
TextColumn::make('cpu')->icon('tabler-cpu'),
TextColumn::make('databases_count')
->counts('databases')
->label('Databases')
->icon('tabler-database')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('backups_count')
TextColumn::make('backups_count')
->counts('backups')
->label('Backups')
->icon('tabler-file-download')

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,12 @@
namespace App\Filament\Resources\ServerResource\Pages;
use App\Models\Database;
use App\Services\Databases\DatabaseManagementService;
use App\Services\Databases\DatabasePasswordService;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Repeater;
use LogicException;
use App\Filament\Resources\ServerResource;
use App\Http\Controllers\Admin\ServersController;
use App\Services\Servers\RandomWordService;
@@ -14,7 +20,6 @@ use App\Enums\ServerState;
use App\Models\Egg;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Servers\ServerDeletionService;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Form;
@@ -38,55 +43,6 @@ class EditServer extends EditRecord
'lg' => 4,
])
->schema([
Forms\Components\ToggleButtons::make('docker')
->label('Container Status')->inline()->inlineLabel()
->formatStateUsing(function ($state, Server $server) {
if ($server->node_id === null) {
return 'unknown';
}
/** @var DaemonServerRepository $service */
$service = resolve(DaemonServerRepository::class);
$details = $service->setServer($server)->getDetails();
return $details['state'] ?? 'unknown';
})
->options(fn ($state) => collect(ContainerStatus::cases())->filter(fn ($containerStatus) => $containerStatus->value === $state)->mapWithKeys(
fn (ContainerStatus $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
))
->colors(collect(ContainerStatus::cases())->mapWithKeys(
fn (ContainerStatus $status) => [$status->value => $status->color()]
))
->icons(collect(ContainerStatus::cases())->mapWithKeys(
fn (ContainerStatus $status) => [$status->value => $status->icon()]
))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 2,
]),
Forms\Components\ToggleButtons::make('status')
->label('Server State')->inline()->inlineLabel()
->helperText('')
->formatStateUsing(fn ($state) => $state ?? ServerState::Normal)
->options(fn ($state) => collect(ServerState::cases())->filter(fn ($serverState) => $serverState->value === $state)->mapWithKeys(
fn (ServerState $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
))
->colors(collect(ServerState::cases())->mapWithKeys(
fn (ServerState $state) => [$state->value => $state->color()]
))
->icons(collect(ServerState::cases())->mapWithKeys(
fn (ServerState $state) => [$state->value => $state->icon()]
))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 2,
]),
Tabs::make('Tabs')
->persistTabInQueryString()
->columnSpan(6)
@@ -115,27 +71,47 @@ class EditServer extends EditRecord
}))
->columnSpan([
'default' => 2,
'sm' => 2,
'sm' => 1,
'md' => 2,
'lg' => 3,
])
->required()
->maxLength(191),
->maxLength(255),
Forms\Components\Select::make('owner_id')
->prefixIcon('tabler-user')
->label('Owner')
->columnSpan([
'default' => 2,
'sm' => 2,
'sm' => 1,
'md' => 2,
'lg' => 3,
'lg' => 2,
])
->relationship('user', 'username')
->searchable()
->preload()
->required(),
Forms\Components\ToggleButtons::make('condition')
->label('Server Status')
->formatStateUsing(fn (Server $server) => $server->condition)
->options(fn ($state) => collect(array_merge(ContainerStatus::cases(), ServerState::cases()))
->filter(fn ($condition) => $condition->value === $state)
->mapWithKeys(fn ($state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()])
)
->colors(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys(
fn ($status) => [$status->value => $status->color()]
))
->icons(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys(
fn ($status) => [$status->value => $status->icon()]
))
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
Forms\Components\Textarea::make('description')
->label('Description')
->columnSpanFull(),
@@ -144,7 +120,7 @@ class EditServer extends EditRecord
->hintAction(CopyAction::make())
->columnSpan([
'default' => 2,
'sm' => 2,
'sm' => 1,
'md' => 2,
'lg' => 3,
])
@@ -154,7 +130,7 @@ class EditServer extends EditRecord
->hintAction(CopyAction::make())
->columnSpan([
'default' => 2,
'sm' => 2,
'sm' => 1,
'md' => 2,
'lg' => 3,
])
@@ -163,17 +139,17 @@ class EditServer extends EditRecord
->label('External ID')
->columnSpan([
'default' => 2,
'sm' => 2,
'sm' => 1,
'md' => 2,
'lg' => 3,
])
->maxLength(191),
->maxLength(255),
Forms\Components\Select::make('node_id')
->label('Node')
->relationship('node', 'name')
->columnSpan([
'default' => 2,
'sm' => 2,
'sm' => 1,
'md' => 2,
'lg' => 3,
])
@@ -219,7 +195,7 @@ class EditServer extends EditRecord
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix('MiB')
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
->columnSpan(2)
->numeric()
@@ -249,7 +225,7 @@ class EditServer extends EditRecord
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix('MiB')
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
->columnSpan(2)
->numeric()
@@ -299,6 +275,7 @@ class EditServer extends EditRecord
'unlimited' => -1,
'disabled' => 0,
'limited' => 128,
default => throw new LogicException('Invalid state')
};
$set('swap', $value);
@@ -308,6 +285,7 @@ class EditServer extends EditRecord
$get('swap') > 0 => 'limited',
$get('swap') == 0 => 'disabled',
$get('swap') < 0 => 'unlimited',
default => throw new LogicException('Invalid state')
};
})
->options([
@@ -325,10 +303,10 @@ class EditServer extends EditRecord
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
'disabled', 'unlimited', true => true,
'limited', false => false,
default => false,
})
->label('Swap Memory')->inlineLabel()
->suffix('MiB')
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->minValue(-1)
->columnSpan(2)
->required()
@@ -378,14 +356,17 @@ class EditServer extends EditRecord
Forms\Components\TextInput::make('allocation_limit')
->suffixIcon('tabler-network')
->required()
->minValue(0)
->numeric(),
Forms\Components\TextInput::make('database_limit')
->suffixIcon('tabler-database')
->required()
->minValue(0)
->numeric(),
Forms\Components\TextInput::make('backup_limit')
->suffixIcon('tabler-copy-check')
->required()
->minValue(0)
->numeric(),
]),
Forms\Components\Fieldset::make('Docker Settings')
@@ -532,6 +513,7 @@ class EditServer extends EditRecord
$text = Forms\Components\TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->required(fn (ServerVariable $serverVariable) => in_array('required', explode('|', $serverVariable->variable->rules)))
->rules([
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
$validator = Validator::make(['validatorkey' => $value], [
@@ -553,7 +535,6 @@ class EditServer extends EditRecord
$components = [$text, $select];
/** @var Forms\Components\Component $component */
foreach ($components as &$component) {
$component = $component
->live(onBlur: true)
@@ -582,9 +563,56 @@ class EditServer extends EditRecord
Tabs\Tab::make('Databases')
->icon('tabler-database')
->schema([
Forms\Components\Placeholder::make('soon')
->label('Soon™'),
]),
Repeater::make('databases')
->grid()
->helperText(fn (Server $server) => $server->databases->isNotEmpty() ? '' : 'No Databases exist for this Server')
->columns(2)
->schema([
Forms\Components\TextInput::make('database')
->columnSpan(2)
->label('Database Name')
->disabled()
->formatStateUsing(fn ($record) => $record->database)
->hintAction(
Action::make('Delete')
->color('danger')
->icon('tabler-trash')
->action(fn (DatabaseManagementService $databaseManagementService, $record) => $databaseManagementService->delete($record))
),
Forms\Components\TextInput::make('username')
->disabled()
->formatStateUsing(fn ($record) => $record->username)
->columnSpan(2),
Forms\Components\TextInput::make('password')
->disabled()
->hintAction(
Action::make('rotate')
->icon('tabler-refresh')
->requiresConfirmation()
->action(fn (DatabasePasswordService $service, $record, $set, $get) => $this->rotatePassword($service, $record, $set, $get))
)
->formatStateUsing(fn (Database $database) => $database->password)
->columnSpan(2),
Forms\Components\TextInput::make('remote')
->disabled()
->formatStateUsing(fn ($record) => $record->remote)
->columnSpan(1)
->label('Connections From'),
Forms\Components\TextInput::make('max_connections')
->disabled()
->formatStateUsing(fn ($record) => $record->max_connections)
->columnSpan(1),
Forms\Components\TextInput::make('JDBC')
->disabled()
->label('JDBC Connection String')
->columnSpan(2)
->formatStateUsing(fn (Forms\Get $get, $record) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($record->password) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database')),
])
->relationship('databases')
->deletable(false)
->addable(false)
->columnSpan(4),
])->columns(4),
Tabs\Tab::make('Actions')
->icon('tabler-settings')
->schema([
@@ -606,7 +634,7 @@ class EditServer extends EditRecord
->action(function (ServersController $serversController, Server $server) {
$serversController->toggleInstall($server);
return $this->refreshFormData(['status', 'docker']);
$this->refreshFormData(['status', 'docker']);
}),
])->fullWidth(),
Forms\Components\ToggleButtons::make('')
@@ -624,7 +652,7 @@ class EditServer extends EditRecord
$suspensionService->toggle($server, 'suspend');
Notification::make()->success()->title('Server Suspended!')->send();
return $this->refreshFormData(['status', 'docker']);
$this->refreshFormData(['status', 'docker']);
}),
Forms\Components\Actions\Action::make('toggleUnsuspend')
->label('Unsuspend')
@@ -634,7 +662,7 @@ class EditServer extends EditRecord
$suspensionService->toggle($server, 'unsuspend');
Notification::make()->success()->title('Server Unsuspended!')->send();
return $this->refreshFormData(['status', 'docker']);
$this->refreshFormData(['status', 'docker']);
}),
])->fullWidth(),
Forms\Components\ToggleButtons::make('')
@@ -650,7 +678,7 @@ class EditServer extends EditRecord
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('transfer')
->label('Transfer Soon™')
->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, $data))
->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
->disabled() //TODO!
->form([ //TODO!
Forms\Components\Select::make('newNode')
@@ -704,7 +732,7 @@ class EditServer extends EditRecord
protected function transferServer(Form $form): Form
{
return $form
->columns(2)
->columns()
->schema([
Forms\Components\Select::make('toNode')
->label('New Node'),
@@ -719,6 +747,8 @@ class EditServer extends EditRecord
Actions\DeleteAction::make('Delete')
->successRedirectUrl(route('filament.admin.resources.servers.index'))
->color('danger')
->disabled(fn (Server $server) => $server->databases()->count() > 0)
->label(fn (Server $server) => $server->databases()->count() > 0 ? 'Server has a Database' : 'Delete')
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
->requiresConfirmation(),
Actions\Action::make('console')
@@ -782,4 +812,13 @@ class EditServer extends EditRecord
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
protected function rotatePassword(DatabasePasswordService $service, $record, $set, $get): void
{
$newPassword = $service->handle($record);
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database');
$set('password', $newPassword);
$set('JDBC', $jdbcString);
}
}

View File

@@ -7,6 +7,7 @@ use App\Models\Server;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Filament\Tables;
@@ -18,35 +19,18 @@ class ListServers extends ListRecords
{
return $table
->searchable(false)
->defaultGroup('node.name')
->groups([
Group::make('node.name')->getDescriptionFromRecordUsing(fn (Server $server): string => str($server->node->description)->limit(150)),
Group::make('user.username')->getDescriptionFromRecordUsing(fn (Server $server): string => $server->user->email),
Group::make('egg.name')->getDescriptionFromRecordUsing(fn (Server $server): string => str($server->egg->description)->limit(150)),
])
->columns([
Tables\Columns\TextColumn::make('status')
Tables\Columns\TextColumn::make('condition')
->default('unknown')
->badge()
->default(function (Server $server) {
if ($server->status !== null) {
return $server->status;
}
return $server->retrieveStatus() ?? 'node_fail';
})
->icon(fn ($state) => match ($state) {
'node_fail' => 'tabler-server-off',
'running' => 'tabler-heartbeat',
'removing' => 'tabler-heart-x',
'offline' => 'tabler-heart-off',
'paused' => 'tabler-heart-pause',
'installing' => 'tabler-heart-bolt',
'suspended' => 'tabler-heart-cancel',
default => 'tabler-heart-question',
})
->color(fn ($state): string => match ($state) {
'running' => 'success',
'installing', 'restarting' => 'primary',
'paused', 'removing' => 'warning',
'node_fail', 'install_failed', 'suspended' => 'danger',
default => 'gray',
}),
->icon(fn (Server $server) => $server->conditionIcon())
->color(fn (Server $server) => $server->conditionColor()),
Tables\Columns\TextColumn::make('uuid')
->hidden()
->label('UUID')
@@ -58,19 +42,25 @@ class ListServers extends ListRecords
Tables\Columns\TextColumn::make('node.name')
->icon('tabler-server-2')
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
->sortable(),
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'node.name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('egg.name')
->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
->sortable(),
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'egg.name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('user.username')
->icon('tabler-user')
->label('Owner')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->sortable(),
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'user.username')
->sortable()
->searchable(),
Tables\Columns\SelectColumn::make('allocation_id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => $server->allocations->mapWithKeys(
->options(fn (Server $server) => $server->allocations->mapWithKeys(
fn ($allocation) => [$allocation->id => $allocation->address])
)
->selectablePlaceholder(false)
@@ -83,9 +73,6 @@ class ListServers extends ListRecords
->numeric()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\Action::make('View')
->icon('tabler-terminal')
@@ -93,6 +80,7 @@ class ListServers extends ListRecords
Tables\Actions\EditAction::make(),
])
->emptyStateIcon('tabler-brand-docker')
->searchable()
->emptyStateDescription('')
->emptyStateHeading('No Servers')
->emptyStateActions([

View File

@@ -3,12 +3,16 @@
namespace App\Filament\Resources\ServerResource\RelationManagers;
use App\Models\Allocation;
use App\Models\Server;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
/**
* @method Server getOwnerRecord()
*/
class AllocationsRelationManager extends RelationManager
{
protected static string $relationship = 'allocations';
@@ -38,12 +42,12 @@ class AllocationsRelationManager extends RelationManager
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
Tables\Columns\IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
false => 'tabler-star',
true => 'tabler-star-filled',
default => 'tabler-star',
})
->color(fn ($state) => match ($state) {
false => 'gray',
true => 'warning',
default => 'gray',
})
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
@@ -68,7 +72,6 @@ class AllocationsRelationManager extends RelationManager
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DissociateBulkAction::make(),
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}

View File

@@ -3,7 +3,7 @@
namespace App\Filament\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers;
use App\Filament\Resources\UserResource\RelationManagers\ServersRelationManager;
use App\Models\User;
use Filament\Resources\Resource;
@@ -23,7 +23,7 @@ class UserResource extends Resource
public static function getRelations(): array
{
return [
RelationManagers\ServersRelationManager::class,
ServersRelationManager::class,
];
}

View File

@@ -13,6 +13,7 @@ use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Common\Version;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use DateTimeZone;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
@@ -31,6 +32,9 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rules\Password;
/**
* @method User getUser()
*/
class EditProfile extends \Filament\Pages\Auth\EditProfile
{
protected function getForms(): array
@@ -49,7 +53,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->label(trans('strings.username'))
->disabled()
->readOnly()
->maxLength(191)
->maxLength(255)
->unique(ignoreRecord: true)
->autofocus(),
@@ -58,7 +62,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->label(trans('strings.email'))
->email()
->required()
->maxLength(191)
->maxLength(255)
->unique(ignoreRecord: true),
TextInput::make('password')
@@ -82,6 +86,12 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->visible(fn (Get $get): bool => filled($get('password')))
->dehydrated(false),
Select::make('timezone')
->required()
->prefixIcon('tabler-clock-pin')
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
->searchable(),
Select::make('language')
->label(trans('strings.language'))
->required()
@@ -117,6 +127,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
];
}
/** @var TwoFactorSetupService */
$setupService = app(TwoFactorSetupService::class);
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
@@ -126,6 +137,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
$options = new QROptions([
'svgLogo' => public_path('pelican.svg'),
'svgLogoScale' => 0.05,
'addLogoSpace' => true,
'logoSpaceWidth' => 13,
'logoSpaceHeight' => 13,
@@ -133,22 +145,24 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
// SVG logo options (see extended class)
$options->svgLogo = public_path('pelican.svg'); // logo from: https://github.com/simple-icons/simple-icons
$options->svgLogoScale = 0.05;
// $options->svgLogoCssClass = 'dark';
// QROptions
// @phpstan-ignore property.protected
$options->version = Version::AUTO;
// $options->outputInterface = QRSvgWithLogo::class;
// @phpstan-ignore property.protected
$options->outputBase64 = false;
// @phpstan-ignore property.protected
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
// @phpstan-ignore property.protected
$options->addQuietzone = true;
// $options->drawLightModules = true;
// @phpstan-ignore property.protected
$options->connectPaths = true;
// @phpstan-ignore property.protected
$options->drawCircularModules = true;
// $options->circleRadius = 0.45;
// @phpstan-ignore property.protected
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
<stop stop-color="#7dd4fc" offset="0"/>
<stop stop-color="#38bdf8" offset="0.5"/>
@@ -186,8 +200,12 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->schema([
Grid::make('asdf')->columns(5)->schema([
Section::make('Create API Key')->columnSpan(3)->schema([
TextInput::make('description')->required(),
TextInput::make('description')
->live(),
TagsInput::make('allowed_ips')
->live()
->splitKeys([',', ' ', 'Tab'])
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
->label('Whitelisted IP\'s')
@@ -195,9 +213,10 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->columnSpanFull(),
])->headerActions([
Action::make('Create')
->disabled(fn (Get $get) => $get('description') === null)
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
->action(function (Get $get, Action $action) {
$token = auth()->user()->createToken(
->action(function (Get $get, Action $action, $user) {
$token = $user->createToken(
$get('description'),
$get('allowed_ips'),
);

View File

@@ -20,8 +20,8 @@ class EditUser extends EditRecord
return $form
->schema([
Section::make()->schema([
Forms\Components\TextInput::make('username')->required()->maxLength(191),
Forms\Components\TextInput::make('email')->email()->required()->maxLength(191),
Forms\Components\TextInput::make('username')->required()->maxLength(255),
Forms\Components\TextInput::make('email')->email()->required()->maxLength(255),
Forms\Components\TextInput::make('password')
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))

View File

@@ -66,7 +66,7 @@ class ListUsers extends ListRecords
->actions([
Tables\Actions\EditAction::make(),
])
->checkIfRecordIsSelectableUsing(fn (User $user) => !$user->servers_count)
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
@@ -85,12 +85,12 @@ class ListUsers extends ListRecords
Forms\Components\TextInput::make('username')
->alphaNum()
->required()
->maxLength(191),
->maxLength(255),
Forms\Components\TextInput::make('email')
->email()
->required()
->unique()
->maxLength(191),
->maxLength(255),
Forms\Components\TextInput::make('password')
->hintIcon('tabler-question-mark')

View File

@@ -68,7 +68,7 @@ class ServersRelationManager extends RelationManager
->sortable(),
Tables\Columns\SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
Tables\Columns\TextColumn::make('image')->hidden(),

View File

@@ -2,15 +2,15 @@
namespace App\Http\Controllers\Admin\Eggs;
use App\Exceptions\Service\Egg\NoParentConfigurationFoundException;
use Illuminate\View\View;
use App\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use App\Http\Controllers\Controller;
use App\Services\Eggs\EggUpdateService;
use App\Services\Eggs\EggCreationService;
use App\Http\Requests\Admin\Egg\EggFormRequest;
use Ramsey\Uuid\Uuid;
class EggController extends Controller
{
@@ -19,8 +19,6 @@ class EggController extends Controller
*/
public function __construct(
protected AlertsMessageBag $alert,
protected EggCreationService $creationService,
protected EggUpdateService $updateService,
protected ViewFactory $view
) {
}
@@ -58,7 +56,16 @@ class EggController extends Controller
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
$data['author'] = $request->user()->email;
$egg = $this->creationService->handle($data);
$data['config_from'] = array_get($data, 'config_from');
if (!is_null($data['config_from'])) {
$parentEgg = Egg::query()->find(array_get($data, 'config_from'));
throw_unless($parentEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
}
$egg = Egg::query()->create(array_merge($data, [
'uuid' => Uuid::uuid4()->toString(),
]));
$this->alert->success(trans('admin/eggs.notices.egg_created'))->flash();
return redirect()->route('admin.eggs.view', $egg->id);
@@ -90,7 +97,13 @@ class EggController extends Controller
$data = $request->validated();
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
$this->updateService->handle($egg, $data);
$eggId = array_get($data, 'config_from');
$copiedFromEgg = Egg::query()->find($eggId);
throw_unless($copiedFromEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
$egg->update($data);
$this->alert->success(trans('admin/eggs.notices.updated'))->flash();
return redirect()->route('admin.eggs.view', $egg->id);

View File

@@ -10,7 +10,6 @@ use Symfony\Component\HttpFoundation\Response;
use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;
use App\Http\Requests\Admin\Egg\EggImportFormRequest;
use App\Services\Eggs\Sharing\EggUpdateImporterService;
class EggShareController extends Controller
{
@@ -21,7 +20,6 @@ class EggShareController extends Controller
protected AlertsMessageBag $alert,
protected EggExporterService $exporterService,
protected EggImporterService $importerService,
protected EggUpdateImporterService $updateImporterService
) {
}
@@ -61,7 +59,7 @@ class EggShareController extends Controller
*/
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
{
$this->updateImporterService->fromFile($egg, $request->file('import_file'));
$this->importerService->fromFile($request->file('import_file'), $egg);
$this->alert->success(trans('admin/eggs.notices.updated_via_import'))->flash();
return redirect()->route('admin.eggs.view', ['egg' => $egg]);

View File

@@ -75,11 +75,11 @@ class ServerManagementController extends ApplicationApiController
if ($this->transferServerService->handle($server, $validatedData)) {
// Transfer started
$this->returnNoContent();
} else {
// Node was not viable
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
return $this->returnNoContent();
}
// Node was not viable
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
}
/**

View File

@@ -8,9 +8,9 @@ use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use App\Facades\Activity;
use App\Services\Users\UserUpdateService;
use App\Transformers\Api\Client\AccountTransformer;
use App\Http\Requests\Api\Client\Account\UpdateEmailRequest;
use App\Http\Requests\Api\Client\Account\UpdatePasswordRequest;
use App\Transformers\Api\Client\UserTransformer;
class AccountController extends ClientApiController
{
@@ -25,7 +25,7 @@ class AccountController extends ClientApiController
public function index(Request $request): array
{
return $this->fractal->item($request->user())
->transformWith($this->getTransformer(AccountTransformer::class))
->transformWith($this->getTransformer(UserTransformer::class))
->toArray();
}

View File

@@ -83,7 +83,7 @@ abstract class AbstractLoginController extends Controller
'data' => [
'complete' => true,
'intended' => $this->redirectPath(),
'user' => $user->toVueObject(),
'user' => $user->toReactObject(),
],
]);
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Facades\Socialite;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\Users\UserUpdateService;
use Exception;
use Illuminate\Http\Request;
class OAuthController extends Controller
{
/**
* OAuthController constructor.
*/
public function __construct(
private AuthManager $auth,
private UserUpdateService $updateService
) {
}
/**
* Redirect user to the OAuth provider
*/
protected function redirect(string $driver): RedirectResponse
{
return Socialite::with($driver)->redirect();
}
/**
* Callback from OAuth provider.
*/
protected function callback(Request $request, string $driver): RedirectResponse
{
$oauthUser = Socialite::driver($driver)->user();
// User is already logged in and wants to link a new OAuth Provider
if ($request->user()) {
$oauth = $request->user()->oauth;
$oauth[$driver] = $oauthUser->getId();
$this->updateService->handle($request->user(), ['oauth' => $oauth]);
return redirect()->route('account');
}
try {
$user = User::query()->whereJsonContains('oauth->'. $driver, $oauthUser->getId())->firstOrFail();
$this->auth->guard()->login($user, true);
} catch (Exception $e) {
// No user found - redirect to normal login
return redirect()->route('auth.login');
}
return redirect('/');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Base;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Facades\Socialite;
use App\Http\Controllers\Controller;
use App\Services\Users\UserUpdateService;
use Illuminate\Http\Response;
class OAuthController extends Controller
{
/**
* OAuthController constructor.
*/
public function __construct(
private UserUpdateService $updateService
) {
}
/**
* Link a new OAuth
*/
protected function link(Request $request): RedirectResponse
{
$driver = $request->get('driver');
return Socialite::with($driver)->redirect();
}
/**
* Remove a OAuth link
*/
protected function unlink(Request $request): Response
{
$oauth = $request->user()->oauth;
unset($oauth[$request->get('driver')]);
$this->updateService->handle($request->user(), ['oauth' => $oauth]);
return new Response('', Response::HTTP_NO_CONTENT);
}
}

View File

@@ -9,14 +9,14 @@ class EggFormRequest extends AdminFormRequest
public function rules(): array
{
$rules = [
'name' => 'required|string|max:191',
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'docker_images' => 'required|string',
'force_outgoing_ip' => 'sometimes|boolean',
'file_denylist' => 'array',
'startup' => 'required|string',
'config_from' => 'sometimes|bail|nullable|numeric',
'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_stop' => 'required_without:config_from|nullable|string|max:255',
'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json',

View File

@@ -13,9 +13,9 @@ class EggVariableFormRequest extends AdminFormRequest
public function rules(): array
{
return [
'name' => 'required|string|min:1|max:191',
'name' => 'required|string|min:1|max:255',
'description' => 'sometimes|nullable|string',
'env_variable' => 'required|regex:/^[\w]{1,191}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . EggVariable::RESERVED_ENV_NAMES,
'options' => 'sometimes|required|array',
'rules' => 'bail|required|string',
'default_value' => 'present',

View File

@@ -10,7 +10,7 @@ class AllocationFormRequest extends AdminFormRequest
{
return [
'allocation_ip' => 'required|string',
'allocation_alias' => 'sometimes|nullable|string|max:191',
'allocation_alias' => 'sometimes|nullable|string|max:255',
'allocation_ports' => 'required|array',
];
}

View File

@@ -13,8 +13,8 @@ class AdvancedSettingsFormRequest extends AdminFormRequest
{
return [
'recaptcha:enabled' => 'required|in:true,false',
'recaptcha:secret_key' => 'required|string|max:191',
'recaptcha:website_key' => 'required|string|max:191',
'recaptcha:secret_key' => 'required|string|max:255',
'recaptcha:website_key' => 'required|string|max:255',
'panel:guzzle:timeout' => 'required|integer|between:1,60',
'panel:guzzle:connect_timeout' => 'required|integer|between:1,60',
'panel:client_features:allocations:enabled' => 'required|in:true,false',

View File

@@ -13,7 +13,7 @@ class BaseSettingsFormRequest extends AdminFormRequest
public function rules(): array
{
return [
'app:name' => 'required|string|max:191',
'app:name' => 'required|string|max:255',
'panel:auth:2fa_required' => 'required|integer|in:0,1,2',
'app:locale' => ['required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],
];

View File

@@ -16,10 +16,10 @@ class MailSettingsFormRequest extends AdminFormRequest
'mail:mailers:smtp:host' => 'required|string',
'mail:mailers:smtp:port' => 'required|integer|between:1,65535',
'mail:mailers:smtp:encryption' => ['present', Rule::in([null, 'tls', 'ssl'])],
'mail:mailers:smtp:username' => 'nullable|string|max:191',
'mail:mailers:smtp:password' => 'nullable|string|max:191',
'mail:mailers:smtp:username' => 'nullable|string|max:255',
'mail:mailers:smtp:password' => 'nullable|string|max:255',
'mail:from:address' => 'required|string|email',
'mail:from:name' => 'nullable|string|max:191',
'mail:from:name' => 'nullable|string|max:255',
];
}

View File

@@ -15,7 +15,7 @@ class StoreAllocationRequest extends ApplicationApiRequest
{
return [
'ip' => 'required|string',
'alias' => 'sometimes|nullable|string|max:191',
'alias' => 'sometimes|nullable|string|max:255',
'ports' => 'required|array',
'ports.*' => 'string',
];

View File

@@ -7,14 +7,13 @@ use App\Models\Mount;
class UpdateMountRequest extends StoreMountRequest
{
/**
* Apply validation rules to this request. Uses the parent class rules()
* function but passes in the rules for updating rather than creating.
* Apply validation rules to this request.
*/
public function rules(array $rules = null): array
{
/** @var Mount $mount */
$mount = $this->route()->parameter('mount');
return parent::rules(Mount::getRulesForUpdate($mount->id));
return Mount::getRulesForUpdate($mount->id);
}
}

View File

@@ -28,8 +28,8 @@ class StoreServerRequest extends ApplicationApiRequest
'description' => array_merge(['nullable'], $rules['description']),
'user' => $rules['owner_id'],
'egg' => $rules['egg_id'],
'docker_image' => $rules['image'],
'startup' => $rules['startup'],
'docker_image' => 'sometimes|string',
'startup' => 'sometimes|string',
'environment' => 'present|array',
'skip_scripts' => 'sometimes|boolean',
'oom_killer' => 'sometimes|boolean',

View File

@@ -20,10 +20,10 @@ class UpdateServerStartupRequest extends ApplicationApiRequest
$data = Server::getRulesForUpdate($this->parameter('server', Server::class));
return [
'startup' => $data['startup'],
'startup' => 'sometimes|string',
'environment' => 'present|array',
'egg' => $data['egg_id'],
'image' => $data['image'],
'image' => 'sometimes|string',
'skip_scripts' => 'present|boolean',
];
}

View File

@@ -25,6 +25,7 @@ class StoreUserRequest extends ApplicationApiRequest
'username',
'password',
'language',
'timezone',
'root_admin',
])->toArray();

View File

@@ -15,7 +15,7 @@ class StoreBackupRequest extends ClientApiRequest
public function rules(): array
{
return [
'name' => 'nullable|string|max:191',
'name' => 'nullable|string|max:255',
'is_locked' => 'nullable|boolean',
'ignored' => 'nullable|string',
];

View File

@@ -14,7 +14,7 @@ class StoreSubuserRequest extends SubuserRequest
public function rules(): array
{
return [
'email' => 'required|email|between:1,191',
'email' => 'required|email|between:1,255',
'permissions' => 'required|array',
'permissions.*' => 'string',
];

View File

@@ -27,6 +27,7 @@ class AssetComposer
'enabled' => config('recaptcha.enabled', false),
'siteKey' => config('recaptcha.website_key') ?? '',
],
'usesSyncDriver' => config('queue.default') === 'sync',
]);
}
}

View File

@@ -24,7 +24,7 @@ class RunTaskJob extends Job implements ShouldQueue
*/
public function __construct(public Task $task, public bool $manualRun = false)
{
$this->queue = 'standard';
}
/**

View File

@@ -103,7 +103,8 @@ class Allocation extends Model
return !is_null($this->ip_alias);
}
public function address(): Attribute
/** @return Attribute<string, never> */
protected function address(): Attribute
{
return Attribute::make(
get: fn () => "$this->ip:$this->port",

View File

@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int $key_type
* @property string $identifier
* @property string $token
* @property array|null $allowed_ips
* @property array $allowed_ips
* @property string|null $memo
* @property \Illuminate\Support\Carbon|null $last_used_at
* @property \Illuminate\Support\Carbon|null $expires_at
@@ -113,6 +113,13 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_MOUNTS,
];
/**
* Default attributes when creating a new model.
*/
protected $attributes = [
'allowed_ips' => '[]',
];
/**
* Fields that should not be included when calling toArray() or toJson()
* on this model.
@@ -128,7 +135,7 @@ class ApiKey extends Model
'identifier' => 'required|string|size:16|unique:api_keys,identifier',
'token' => 'required|string',
'memo' => 'required|nullable|string|max:500',
'allowed_ips' => 'nullable|array',
'allowed_ips' => 'array',
'allowed_ips.*' => 'string',
'last_used_at' => 'nullable|date',
'expires_at' => 'nullable|date',

View File

@@ -16,8 +16,8 @@ class AuditLog extends Model
public static array $validationRules = [
'uuid' => 'required|uuid',
'action' => 'required|string|max:191',
'subaction' => 'nullable|string|max:191',
'action' => 'required|string|max:255',
'subaction' => 'nullable|string|max:255',
'device' => 'array',
'device.ip_address' => 'ip',
'device.user_agent' => 'string',

View File

@@ -46,7 +46,7 @@ class DatabaseHost extends Model
* Validation rules to assign to this model.
*/
public static array $validationRules = [
'name' => 'required|string|max:191',
'name' => 'required|string|max:255',
'host' => 'required|string',
'port' => 'required|numeric|between:1,65535',
'username' => 'required|string|max:32',

View File

@@ -105,7 +105,7 @@ class Egg extends Model
public static array $validationRules = [
'uuid' => 'required|string|size:36',
'name' => 'required|string|max:191',
'name' => 'required|string|max:255',
'description' => 'string|nullable',
'features' => 'array|nullable',
'author' => 'required|string|email',
@@ -115,7 +115,7 @@ class Egg extends Model
'docker_images.*' => 'required|string',
'startup' => 'required|nullable|string',
'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_stop' => 'required_without:config_from|nullable|string|max:255',
'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json',

View File

@@ -52,9 +52,9 @@ class EggVariable extends Model
public static array $validationRules = [
'egg_id' => 'exists:eggs,id',
'sort' => 'nullable',
'name' => 'required|string|between:1,191',
'name' => 'required|string|between:1,255',
'description' => 'string',
'env_variable' => 'required|alphaDash|between:1,191|notIn:' . self::RESERVED_ENV_NAMES,
'env_variable' => 'required|alphaDash|between:1,255|notIn:' . self::RESERVED_ENV_NAMES,
'default_value' => 'string',
'user_viewable' => 'boolean',
'user_editable' => 'boolean',

View File

@@ -2,7 +2,11 @@
namespace App\Models;
use Carbon\CarbonInterface;
use DateTimeInterface;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Container\Container;
@@ -12,6 +16,7 @@ use App\Exceptions\Model\DataValidationException;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Illuminate\Validation\Factory as ValidationFactory;
use Illuminate\Validation\Validator;
use InvalidArgumentException;
abstract class Model extends IlluminateModel
{
@@ -64,6 +69,36 @@ abstract class Model extends IlluminateModel
return 'uuid';
}
protected function asDateTime($value)
{
$timezone = auth()->user()?->timezone ?? config('app.timezone', 'UTC');
if ($value instanceof CarbonInterface) {
return Date::instance($value->timezone($timezone));
}
if ($value instanceof DateTimeInterface) {
return Date::parse($value->format('Y-m-d H:i:s.u'), $timezone);
}
if (is_numeric($value)) {
return Date::createFromTimestamp($value, $timezone);
}
if ($this->isStandardDateFormat($value)) {
return Date::instance(Carbon::createFromFormat('Y-m-d', $value)->timezone($timezone)->startOfDay());
}
$format = $this->getDateFormat();
try {
$date = Date::createFromFormat($format, $value)->timezone($timezone);
} catch (InvalidArgumentException) {
$date = false;
}
return $date ?: Date::parse($value);
}
/**
* Returns the validator instance used by this model.
*/

View File

@@ -41,7 +41,7 @@ class Mount extends Model
*/
public static array $validationRules = [
'name' => 'required|string|min:2|max:64|unique:mounts,name',
'description' => 'nullable|string|max:191',
'description' => 'nullable|string|max:255',
'source' => 'required|string',
'target' => 'required|string',
'read_only' => 'sometimes|boolean',

View File

@@ -35,6 +35,7 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
* @property int $daemon_sftp
* @property string|null $daemon_sftp_alias
* @property string $daemon_base
* @property array $tags
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \App\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts
@@ -82,7 +83,7 @@ class Node extends Model
'description' => 'string|nullable',
'public' => 'boolean',
'fqdn' => 'required|string',
'scheme' => 'required',
'scheme' => 'required|string|in:http,https',
'behind_proxy' => 'boolean',
'memory' => 'required|numeric|min:0',
'memory_overallocate' => 'required|numeric|min:-1',
@@ -133,6 +134,10 @@ class Node extends Model
];
}
public int $servers_sum_memory = 0;
public int $servers_sum_disk = 0;
public int $servers_sum_cpu = 0;
public function getRouteKeyName(): string
{
return 'id';

View File

@@ -76,7 +76,7 @@ class Schedule extends Model
public static array $validationRules = [
'server_id' => 'required|exists:servers,id',
'name' => 'required|string|max:191',
'name' => 'required|string|max:255',
'cron_day_of_week' => 'required|string',
'cron_month' => 'required|string',
'cron_day_of_month' => 'required|string',

View File

@@ -2,9 +2,11 @@
namespace App\Models;
use App\Enums\ContainerStatus;
use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause;
@@ -26,7 +28,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @property int $node_id
* @property string $name
* @property string $description
* @property string|null $status
* @property ServerState|null $status
* @property bool $skip_scripts
* @property int $owner_id
* @property int $memory
@@ -69,7 +71,6 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @property \App\Models\User $user
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\EggVariable[] $variables
* @property int|null $variables_count
*
* @method static \Database\Factories\ServerFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Builder|Server newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Server newQuery()
@@ -100,7 +101,17 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
* @method static \Illuminate\Database\Eloquent\Builder|Server whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereUuid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereuuid_short($value)
*
* @property array|null $docker_labels
* @property string|null $ports
* @property-read mixed $condition
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\EggVariable> $eggVariables
* @property-read int|null $egg_variables_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ServerVariable> $serverVariables
* @property-read int|null $server_variables_count
* @method static \Illuminate\Database\Eloquent\Builder|Server whereDockerLabels($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereInstalledAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server wherePorts($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereUuidShort($value)
* @mixin \Eloquent
*/
class Server extends Model
@@ -139,9 +150,9 @@ class Server extends Model
protected $guarded = ['id', self::CREATED_AT, self::UPDATED_AT, 'deleted_at', 'installed_at'];
public static array $validationRules = [
'external_id' => 'sometimes|nullable|string|between:1,191|unique:servers',
'external_id' => 'sometimes|nullable|string|between:1,255|unique:servers',
'owner_id' => 'required|integer|exists:users,id',
'name' => 'required|string|min:1|max:191',
'name' => 'required|string|min:1|max:255',
'node_id' => 'required|exists:nodes,id',
'description' => 'string',
'status' => 'nullable|string',
@@ -156,7 +167,7 @@ class Server extends Model
'egg_id' => 'required|exists:eggs,id',
'startup' => 'required|string',
'skip_scripts' => 'sometimes|boolean',
'image' => 'required|string|max:191',
'image' => 'required|string|max:255',
'database_limit' => 'present|nullable|integer|min:0',
'allocation_limit' => 'sometimes|nullable|integer|min:0',
'backup_limit' => 'present|nullable|integer|min:0',
@@ -410,4 +421,33 @@ class Server extends Model
return cache()->get("servers.$this->uuid.container.status") ?? 'missing';
}
public function condition(): Attribute
{
return Attribute::make(
get: fn () => $this->status?->value ?? $this->retrieveStatus(),
);
}
public function conditionIcon(): string
{
if ($this->status === null) {
$containerStatus = ContainerStatus::from($this->retrieveStatus());
return $containerStatus->icon();
}
return $this->status->icon();
}
public function conditionColor(): string
{
if ($this->status === null) {
$containerStatus = ContainerStatus::from($this->retrieveStatus());
return $containerStatus->color();
}
return $this->status->color();
}
}

View File

@@ -21,7 +21,7 @@ class Setting extends Model
protected $fillable = ['key', 'value'];
public static array $validationRules = [
'key' => 'required|string|between:1,191',
'key' => 'required|string|between:1,255',
'value' => 'string',
];

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Exceptions\DisplayException;
use App\Rules\Username;
use App\Facades\Activity;
use DateTimeZone;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Models\Contracts\HasName;
@@ -38,10 +39,12 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
* @property string $password
* @property string|null $remember_token
* @property string $language
* @property string $timezone
* @property bool $root_admin
* @property bool $use_totp
* @property string|null $totp_secret
* @property \Illuminate\Support\Carbon|null $totp_authenticated_at
* @property array|null $oauth
* @property bool $gravatar
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
@@ -69,6 +72,7 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
* @method static Builder|User whereGravatar($value)
* @method static Builder|User whereId($value)
* @method static Builder|User whereLanguage($value)
* @method static Builder|User whereTimezone($value)
* @method static Builder|User whereNameFirst($value)
* @method static Builder|User whereNameLast($value)
* @method static Builder|User wherePassword($value)
@@ -122,17 +126,19 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'name_last',
'password',
'language',
'timezone',
'use_totp',
'totp_secret',
'totp_authenticated_at',
'gravatar',
'root_admin',
'oauth',
];
/**
* The attributes excluded from the model's JSON form.
*/
protected $hidden = ['password', 'remember_token', 'totp_secret', 'totp_authenticated_at'];
protected $hidden = ['password', 'remember_token', 'totp_secret', 'totp_authenticated_at', 'oauth'];
/**
* Default values for specific fields in the database.
@@ -141,10 +147,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'external_id' => null,
'root_admin' => false,
'language' => 'en',
'timezone' => 'UTC',
'use_totp' => false,
'totp_secret' => null,
'name_first' => '',
'name_last' => '',
'oauth' => '[]',
];
/**
@@ -152,16 +160,18 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/
public static array $validationRules = [
'uuid' => 'nullable|string|size:36|unique:users,uuid',
'email' => 'required|email|between:1,191|unique:users,email',
'external_id' => 'sometimes|nullable|string|max:191|unique:users,external_id',
'username' => 'required|between:1,191|unique:users,username',
'name_first' => 'nullable|string|between:0,191',
'name_last' => 'nullable|string|between:0,191',
'email' => 'required|email|between:1,255|unique:users,email',
'external_id' => 'sometimes|nullable|string|max:255|unique:users,external_id',
'username' => 'required|between:1,255|unique:users,username',
'name_first' => 'nullable|string|between:0,255',
'name_last' => 'nullable|string|between:0,255',
'password' => 'sometimes|nullable|string',
'root_admin' => 'boolean',
'language' => 'string',
'timezone' => 'string',
'use_totp' => 'boolean',
'totp_secret' => 'nullable|string',
'oauth' => 'array|nullable',
];
protected function casts(): array
@@ -172,6 +182,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'gravatar' => 'boolean',
'totp_authenticated_at' => 'datetime',
'totp_secret' => 'encrypted',
'oauth' => 'array',
];
}
@@ -204,15 +215,16 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
$rules = parent::getRules();
$rules['language'][] = new In(array_keys((new self())->getAvailableLanguages()));
$rules['timezone'][] = new In(array_values(DateTimeZone::listIdentifiers()));
$rules['username'][] = new Username();
return $rules;
}
/**
* Return the user model in a format that can be passed over to Vue templates.
* Return the user model in a format that can be passed over to React templates.
*/
public function toVueObject(): array
public function toReactObject(): array
{
return collect($this->toArray())->except(['id', 'external_id'])->toArray();
}

View File

@@ -13,9 +13,9 @@ use Dedoc\Scramble\Support\Generator\SecurityScheme;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@@ -29,8 +29,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Schema::defaultStringLength(191);
$versionData = app(SoftwareVersionService::class)->versionData();
View::share('appVersion', $versionData['version'] ?? 'undefined');
View::share('appIsGit', $versionData['is_git'] ?? false);
@@ -78,6 +76,10 @@ class AppServiceProvider extends ServiceProvider
Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']]);
Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
Scramble::registerApi('remote', ['api_path' => 'api/remote', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class);
});
}
/**

View File

@@ -25,9 +25,9 @@ class DaemonServerRepository extends DaemonRepository
Assert::isInstanceOf($this->server, Server::class);
try {
$response = $this->getHttpClient()->get(
return $this->getHttpClient()->get(
sprintf('/api/servers/%s', $this->server->uuid)
)->throw();
)->throw()->json();
} catch (RequestException $exception) {
$cfId = $exception->response->header('Cf-Ray');
$cfCache = $exception->response->header('Cf-Cache-Status');
@@ -48,7 +48,7 @@ class DaemonServerRepository extends DaemonRepository
report($exception);
}
return $response?->json() ?? ['state' => ContainerStatus::Missing->value];
return ['state' => ContainerStatus::Missing->value];
}
/**

View File

@@ -33,13 +33,13 @@ class HostCreationService
'host' => array_get($data, 'host'),
'port' => array_get($data, 'port'),
'username' => array_get($data, 'username'),
'max_databases' => null,
'max_databases' => array_get($data, 'max_databases'),
'node_id' => array_get($data, 'node_id'),
]);
// Confirm access using the provided credentials before saving data.
$this->dynamic->set('dynamic', $host);
$this->databaseManager->connection('dynamic')->select('SELECT 1 FROM dual');
$this->databaseManager->connection('dynamic')->getPdo();
return $host;
});

View File

@@ -24,18 +24,21 @@ class HostUpdateService
*
* @throws \Throwable
*/
public function handle(int $hostId, array $data): DatabaseHost
public function handle(DatabaseHost|int $host, array $data): DatabaseHost
{
if (!$host instanceof DatabaseHost) {
$host = DatabaseHost::query()->findOrFail($host);
}
if (empty(array_get($data, 'password'))) {
unset($data['password']);
}
return $this->connection->transaction(function () use ($data, $hostId) {
/** @var DatabaseHost $host */
$host = DatabaseHost::query()->findOrFail($hostId);
return $this->connection->transaction(function () use ($data, $host) {
$host->update($data);
$this->dynamic->set('dynamic', $host);
$this->databaseManager->connection('dynamic')->select('SELECT 1 FROM dual');
$this->databaseManager->connection('dynamic')->getPdo();
return $host;
});

View File

@@ -164,7 +164,7 @@ class EggConfigurationService
// variable from the server configuration.
$plucked = Arr::get(
$structure,
preg_replace('/^env\./', 'build.environment.', $key),
preg_replace('/^env\./', 'environment.', $key),
''
);

View File

@@ -1,29 +0,0 @@
<?php
namespace App\Services\Eggs;
use Ramsey\Uuid\Uuid;
use App\Models\Egg;
use App\Exceptions\Service\Egg\NoParentConfigurationFoundException;
class EggCreationService
{
/**
* Create a new egg.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Egg\NoParentConfigurationFoundException
*/
public function handle(array $data): Egg
{
$data['config_from'] = array_get($data, 'config_from');
if (!is_null($data['config_from'])) {
$parentEgg = Egg::query()->find(array_get($data, 'config_from'));
throw_unless($parentEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
}
return Egg::query()->create(array_merge($data, [
'uuid' => Uuid::uuid4()->toString(),
]));
}
}

View File

@@ -1,107 +0,0 @@
<?php
namespace App\Services\Eggs;
use Illuminate\Support\Arr;
use App\Models\Egg;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use App\Exceptions\Service\InvalidFileUploadException;
class EggParserService
{
public const UPGRADE_VARIABLES = [
'server.build.env.SERVER_IP' => 'server.allocations.default.ip',
'server.build.default.ip' => 'server.allocations.default.ip',
'server.build.env.SERVER_PORT' => 'server.allocations.default.port',
'server.build.default.port' => 'server.allocations.default.port',
'server.build.env.SERVER_MEMORY' => 'server.build.memory_limit',
'server.build.memory' => 'server.build.memory_limit',
'server.build.env' => 'server.build.environment',
];
/**
* Takes an uploaded file and parses out the egg configuration from within.
*
* @throws \JsonException
* @throws \App\Exceptions\Service\InvalidFileUploadException
*/
public function handle(UploadedFile $file): array
{
if ($file->getError() !== UPLOAD_ERR_OK) {
throw new InvalidFileUploadException('The selected file was not uploaded successfully');
}
$parsed = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR);
$version = $parsed['meta']['version'] ?? '';
$parsed = match ($version) {
'PTDL_v1' => $this->convertToV2($parsed),
'PTDL_v2' => $parsed,
default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.')
};
// Make sure we only use recent variable format from now on
$parsed['config']['files'] = str_replace(
array_keys(self::UPGRADE_VARIABLES),
array_values(self::UPGRADE_VARIABLES),
$parsed['config']['files'] ?? '',
);
return $parsed;
}
/**
* Fills the provided model with the parsed JSON data.
*/
public function fillFromParsed(Egg $model, array $parsed): Egg
{
return $model->forceFill([
'name' => Arr::get($parsed, 'name'),
'description' => Arr::get($parsed, 'description'),
'features' => Arr::get($parsed, 'features'),
'docker_images' => Arr::get($parsed, 'docker_images'),
'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist'))
->filter(fn ($value) => !empty($value)),
'update_url' => Arr::get($parsed, 'meta.update_url'),
'config_files' => Arr::get($parsed, 'config.files'),
'config_startup' => Arr::get($parsed, 'config.startup'),
'config_logs' => Arr::get($parsed, 'config.logs'),
'config_stop' => Arr::get($parsed, 'config.stop'),
'startup' => Arr::get($parsed, 'startup'),
'script_install' => Arr::get($parsed, 'scripts.installation.script'),
'script_entry' => Arr::get($parsed, 'scripts.installation.entrypoint'),
'script_container' => Arr::get($parsed, 'scripts.installation.container'),
]);
}
/**
* Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles
* the "docker_images" field potentially not being present, and not being in the
* expected "key => value" format.
*/
protected function convertToV2(array $parsed): array
{
// Maintain backwards compatability for eggs that are still using the old single image
// string format. New eggs can provide an array of Docker images that can be used.
if (!isset($parsed['images'])) {
$images = [Arr::get($parsed, 'image') ?? 'nil'];
} else {
$images = $parsed['images'];
}
unset($parsed['images'], $parsed['image']);
$parsed['docker_images'] = [];
foreach ($images as $image) {
$parsed['docker_images'][$image] = $image;
}
$parsed['variables'] = array_map(function ($value) {
return array_merge($value, ['field_type' => 'text']);
}, $parsed['variables']);
return $parsed;
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Services\Eggs;
use App\Models\Egg;
use App\Exceptions\Service\Egg\NoParentConfigurationFoundException;
class EggUpdateService
{
/**
* Update an egg.
*/
public function handle(Egg $egg, array $data): void
{
$eggId = array_get($data, 'config_from');
$copiedFromEgg = Egg::query()->find($eggId);
throw_unless($copiedFromEgg, new NoParentConfigurationFoundException(trans('exceptions.egg.invalid_copy_id')));
// TODO: Once the admin UI is done being reworked and this is exposed
// in said UI, remove this so that you can actually update the denylist.
unset($data['file_denylist']);
$egg->update($data);
}
}

View File

@@ -2,18 +2,30 @@
namespace App\Services\Eggs\Sharing;
use App\Exceptions\Service\InvalidFileUploadException;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
use App\Models\Egg;
use Illuminate\Http\UploadedFile;
use App\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use App\Services\Eggs\EggParserService;
use Illuminate\Support\Collection;
use Spatie\TemporaryDirectory\TemporaryDirectory;
class EggImporterService
{
public function __construct(protected ConnectionInterface $connection, protected EggParserService $parser)
public const UPGRADE_VARIABLES = [
'server.build.env.SERVER_IP' => 'server.allocations.default.ip',
'server.build.default.ip' => 'server.allocations.default.ip',
'server.build.env.SERVER_PORT' => 'server.allocations.default.port',
'server.build.default.port' => 'server.allocations.default.port',
'server.build.env.SERVER_MEMORY' => 'server.build.memory_limit',
'server.build.memory' => 'server.build.memory_limit',
'server.build.env.' => 'server.environment.',
'server.build.environment.' => 'server.environment.',
];
public function __construct(protected ConnectionInterface $connection)
{
}
@@ -22,13 +34,13 @@ class EggImporterService
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function fromFile(UploadedFile $file): Egg
public function fromFile(UploadedFile $file, Egg $egg = null): Egg
{
$parsed = $this->parser->handle($file);
$parsed = $this->parseFile($file);
return $this->connection->transaction(function () use ($parsed) {
return $this->connection->transaction(function () use ($egg, $parsed) {
$uuid = $parsed['uuid'] ?? Uuid::uuid4()->toString();
$egg = Egg::where('uuid', $uuid)->first() ?? new Egg();
$egg = $egg ?? Egg::where('uuid', $uuid)->first() ?? new Egg();
$egg = $egg->forceFill([
'uuid' => $uuid,
@@ -36,23 +48,32 @@ class EggImporterService
'copy_script_from' => null,
]);
$egg = $this->parser->fillFromParsed($egg, $parsed);
$egg = $this->fillFromParsed($egg, $parsed);
$egg->save();
// Update existing variables or create new ones.
foreach ($parsed['variables'] ?? [] as $variable) {
EggVariable::query()->forceCreate(array_merge($variable, ['egg_id' => $egg->id]));
EggVariable::unguarded(function () use ($egg, $variable) {
$egg->variables()->updateOrCreate([
'env_variable' => $variable['env_variable'],
], Collection::make($variable)->except(['egg_id', 'env_variable'])->toArray());
});
}
return $egg;
$imported = array_map(fn ($value) => $value['env_variable'], $parsed['variables'] ?? []);
$egg->variables()->whereNotIn('env_variable', $imported)->delete();
return $egg->refresh();
});
}
/**
* Take an url and parse it into a new egg.
* Take an url and parse it into a new egg or update an existing one.
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function fromUrl(string $url): Egg
public function fromUrl(string $url, Egg $egg = null): Egg
{
$info = pathinfo($url);
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
@@ -60,6 +81,91 @@ class EggImporterService
file_put_contents($tmpPath, file_get_contents($url));
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'));
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'), $egg);
}
/**
* Takes an uploaded file and parses out the egg configuration from within.
*
* @throws \JsonException
* @throws \App\Exceptions\Service\InvalidFileUploadException
*/
protected function parseFile(UploadedFile $file): array
{
if ($file->getError() !== UPLOAD_ERR_OK) {
throw new InvalidFileUploadException('The selected file was not uploaded successfully');
}
$parsed = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR);
$version = $parsed['meta']['version'] ?? '';
$parsed = match ($version) {
'PTDL_v1' => $this->convertToV2($parsed),
'PTDL_v2' => $parsed,
default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.')
};
// Make sure we only use recent variable format from now on
$parsed['config']['files'] = str_replace(
array_keys(self::UPGRADE_VARIABLES),
array_values(self::UPGRADE_VARIABLES),
$parsed['config']['files'] ?? '',
);
return $parsed;
}
/**
* Fills the provided model with the parsed JSON data.
*/
protected function fillFromParsed(Egg $model, array $parsed): Egg
{
return $model->forceFill([
'name' => Arr::get($parsed, 'name'),
'description' => Arr::get($parsed, 'description'),
'features' => Arr::get($parsed, 'features'),
'docker_images' => Arr::get($parsed, 'docker_images'),
'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist'))
->filter(fn ($value) => !empty($value)),
'update_url' => Arr::get($parsed, 'meta.update_url'),
'config_files' => Arr::get($parsed, 'config.files'),
'config_startup' => Arr::get($parsed, 'config.startup'),
'config_logs' => Arr::get($parsed, 'config.logs'),
'config_stop' => Arr::get($parsed, 'config.stop'),
'startup' => Arr::get($parsed, 'startup'),
'script_install' => Arr::get($parsed, 'scripts.installation.script'),
'script_entry' => Arr::get($parsed, 'scripts.installation.entrypoint'),
'script_container' => Arr::get($parsed, 'scripts.installation.container'),
]);
}
/**
* Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles
* the "docker_images" field potentially not being present, and not being in the
* expected "key => value" format.
*/
protected function convertToV2(array $parsed): array
{
// Maintain backwards compatability for eggs that are still using the old single image
// string format. New eggs can provide an array of Docker images that can be used.
if (!isset($parsed['images'])) {
$images = [Arr::get($parsed, 'image') ?? 'nil'];
} else {
$images = $parsed['images'];
}
unset($parsed['images'], $parsed['image']);
$parsed['docker_images'] = [];
foreach ($images as $image) {
$parsed['docker_images'][$image] = $image;
}
$parsed['variables'] = array_map(function ($value) {
return array_merge($value, ['field_type' => 'text']);
}, $parsed['variables']);
return $parsed;
}
}

View File

@@ -1,67 +0,0 @@
<?php
namespace App\Services\Eggs\Sharing;
use App\Models\Egg;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
use App\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use App\Services\Eggs\EggParserService;
use Spatie\TemporaryDirectory\TemporaryDirectory;
class EggUpdateImporterService
{
/**
* EggUpdateImporterService constructor.
*/
public function __construct(protected ConnectionInterface $connection, protected EggParserService $parser)
{
}
/**
* Update an existing Egg using an uploaded JSON file.
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function fromFile(Egg $egg, UploadedFile $file): Egg
{
$parsed = $this->parser->handle($file);
return $this->connection->transaction(function () use ($egg, $parsed) {
$egg = $this->parser->fillFromParsed($egg, $parsed);
$egg->save();
// Update existing variables or create new ones.
foreach ($parsed['variables'] ?? [] as $variable) {
EggVariable::unguarded(function () use ($egg, $variable) {
$egg->variables()->updateOrCreate([
'env_variable' => $variable['env_variable'],
], Collection::make($variable)->except(['egg_id', 'env_variable'])->toArray());
});
}
$imported = array_map(fn ($value) => $value['env_variable'], $parsed['variables'] ?? []);
$egg->variables()->whereNotIn('env_variable', $imported)->delete();
return $egg->refresh();
});
}
/**
* Update an existing Egg using an url.
*
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
*/
public function fromUrl(Egg $egg, string $url): Egg
{
$info = pathinfo($url);
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
$tmpPath = $tmpDir->path($info['basename']);
file_put_contents($tmpPath, file_get_contents($url));
return $this->fromFile($egg, new UploadedFile($tmpPath, $info['basename'], 'application/json'));
}
}

View File

@@ -2,12 +2,11 @@
namespace App\Services\Helpers;
use Exception;
use GuzzleHttp\Client;
use Carbon\CarbonImmutable;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Arr;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use App\Exceptions\Service\Helper\CdnVersionFetchingException;
class SoftwareVersionService
{
@@ -87,17 +86,27 @@ class SoftwareVersionService
protected function cacheVersionData(): array
{
return $this->cache->remember(self::VERSION_CACHE_KEY, CarbonImmutable::now()->addMinutes(config('panel.cdn.cache_time', 60)), function () {
try {
$response = $this->client->request('GET', config('panel.cdn.url'));
$versionData = [];
try {
$response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/panel/releases/latest');
if ($response->getStatusCode() === 200) {
return json_decode($response->getBody(), true);
$panelData = json_decode($response->getBody(), true);
$versionData['panel'] = trim($panelData['tag_name'], 'v');
}
throw new CdnVersionFetchingException();
} catch (Exception) {
return [];
$response = $this->client->request('GET', 'https://api.github.com/repos/pelican-dev/wings/releases/latest');
if ($response->getStatusCode() === 200) {
$wingsData = json_decode($response->getBody(), true);
$versionData['daemon'] = trim($wingsData['tag_name'], 'v');
}
} catch (ClientException $e) {
}
$versionData['discord'] = 'https://pelican.dev/discord';
$versionData['donate'] = 'https://pelican.dev/donate';
return $versionData;
});
}

View File

@@ -51,12 +51,12 @@ class ServerConfigurationStructureService
'invocation' => $server->startup,
'skip_egg_scripts' => $server->skip_scripts,
'build' => [
'memory_limit' => $server->memory,
'swap' => $server->swap,
'memory_limit' => config('panel.use_binary_prefix') ? $server->memory : $server->memory / 1.048576,
'swap' => config('panel.use_binary_prefix') ? $server->swap : $server->swap / 1.048576,
'io_weight' => $server->io,
'cpu_limit' => $server->cpu,
'threads' => $server->threads,
'disk_space' => $server->disk,
'disk_space' => config('panel.use_binary_prefix') ? $server->disk : $server->disk / 1.048576,
'oom_killer' => $server->oom_killer,
],
'container' => [

View File

@@ -17,6 +17,7 @@ use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Deployment\FindViableNodesService;
use App\Services\Deployment\AllocationSelectionService;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Models\Egg;
class ServerCreationService
{
@@ -50,6 +51,13 @@ class ServerCreationService
$data['oom_killer'] = !$data['oom_disabled'];
}
/** @var Egg $egg */
$egg = Egg::query()->findOrFail($data['egg_id']);
// Fill missing fields from egg
$data['image'] = $data['image'] ?? collect($egg->docker_images)->first();
$data['startup'] = $data['startup'] ?? $egg->startup;
// If a deployment object has been passed we need to get the allocation
// that the server should use, and assign the node from that allocation.
if ($deployment instanceof DeploymentObject) {

View File

@@ -76,6 +76,10 @@ class StartupModificationService
$server = $server->forceFill([
'egg_id' => $egg->id,
]);
// Fill missing fields from egg
$data['docker_image'] = $data['docker_image'] ?? collect($egg->docker_images)->first();
$data['startup'] = $data['startup'] ?? $egg->startup;
}
$server->fill([

View File

@@ -33,7 +33,7 @@ trait EnvironmentWriterTrait
$saveContents = file_get_contents($path);
collect($values)->each(function ($value, $key) use (&$saveContents) {
$key = strtoupper($key);
$saveValue = sprintf('%s=%s', $key, $this->escapeEnvironmentValue($value));
$saveValue = sprintf('%s=%s', $key, $this->escapeEnvironmentValue($value ?? ''));
if (preg_match_all('/^' . $key . '=(.*)$/m', $saveContents) < 1) {
$saveContents = $saveContents . PHP_EOL . $saveValue;

View File

@@ -37,7 +37,8 @@ class UserTransformer extends BaseTransformer
'last_name' => $user->name_last,
'language' => $user->language,
'root_admin' => (bool) $user->root_admin,
'2fa' => (bool) $user->use_totp,
'2fa_enabled' => (bool) $user->use_totp,
'2fa' => (bool) $user->use_totp, // deprecated, use "2fa_enabled"
'created_at' => $this->formatTimestamp($user->created_at),
'updated_at' => $this->formatTimestamp($user->updated_at),
];

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Transformers\Api\Client;
use App\Models\User;
class AccountTransformer extends BaseClientTransformer
{
/**
* Return the resource name for the JSONAPI output.
*/
public function getResourceName(): string
{
return 'user';
}
/**
* Return basic information about the currently logged-in user.
*/
public function transform(User $model): array
{
return [
'id' => $model->id,
'admin' => $model->root_admin,
'username' => $model->username,
'email' => $model->email,
'first_name' => $model->name_first,
'last_name' => $model->name_last,
'language' => $model->language,
];
}
}

View File

@@ -55,7 +55,7 @@ class ActivityLogTransformer extends BaseClientTransformer
$properties = $model->properties
->mapWithKeys(function ($value, $key) use ($model) {
if ($key === 'ip' && $model->actor && !$model->actor->is($this->request->user())) {
if ($key === 'ip' && !$model->actor->is($this->request->user())) {
return [$key => '[hidden]'];
}

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