Compare commits

..

265 Commits

Author SHA1 Message Date
Boy132
953ee940aa Installer followup (#519)
* remove queue worker service creation from installer

* auto check redis
2024-08-04 18:53:54 +02:00
Boy132
496eaaaf83 Web Installer (#504)
* simplify setup command

* add installer page

* add route for installer

* adjust gitignore

* set colors globally

* add "unsaved data changes" alert

* add helper method to check if panel is installed

* make nicer

* redis username isn't required

* bring back db settings command

* store current date in "installed" file

* only redirect if install was successfull

* remove fpm requirement

* change "installed" marker to env variable

* improve requirements step

* add commands to change cache, queue or session drivers respectively

* removed `grouped` for better mobile view
2024-08-03 21:13:17 +02:00
MartinOscar
18cf6e9338 Update SetupTOTPDialog.tsx (#518) 2024-07-31 15:10:58 -04:00
Charles
525a106e81 Change TextArea -> Textarea...
Makes no sense as we have TextInput, TagsInput and KeyValue... But TextArea is an issue...
2024-07-30 14:12:29 -04:00
Charles
d22f975684 More Mobile UI
Closes https://github.com/pelican-dev/panel/issues/512
2024-07-30 12:58:16 -04:00
Charles
c4864feaa5 Whoops 2024-07-30 10:45:12 -04:00
Charles
b7b72d7336 Merge branch 'main' of https://github.com/pelican-dev/panel 2024-07-30 10:43:30 -04:00
Charles
686c4375bc Layout fix for mobile 2024-07-30 10:43:24 -04:00
Boy132
3f40256f8b Settings page followup (#514)
* remove group for toggle buttons

* fix default for APP_DEBUG

* correctly handle bool values

* fix pint

* small cleanup for example .env
2024-07-30 16:07:20 +02:00
Boy132
a58e159478 Settings page (#486)
* remove old settings stuff

* add basic settings page

* add some settings

* add "test mail" button

* fix mail fields not updating

* fix phpstan

* fix default for "top navigation"

* force toggle buttons to be bool

* force toggle to be bool

* add class to view to allow customization

* add mailgun settings

* add notification settings

* add timeout settings

* organize tabs into sub-functions

* add more settings

* add backup settings

* add sections to mail settings

* add setting for trusted_proxies

* fix unsaved data alert not showing

* fix clear action

* Fix clear action v2

TagsInput expects an array, not a string, fails on saving when using `''`

* Add App favicon

* Remove defaults, collapse misc sections

* Move Save btn, Add API rate limit

* small cleanup

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-07-29 12:14:24 +02:00
Boy132
d89af243a8 Fix user search on "create server" (#508) 2024-07-29 12:13:29 +02:00
Boy132
bddd6af8af Fix user deletion in no interactive mode (#506) 2024-07-29 12:13:08 +02:00
MartinOscar
e1bdf95971 Update SetupTOTPDialog.tsx (#476) 2024-07-29 05:58:20 -04:00
Lance Pioch
465a03bf0e Update readme.md 2024-07-24 20:10:45 -04:00
Boy132
2c2e52b18a fix phpstan (#503) 2024-07-23 11:32:32 +02:00
notCharles
fcef8d69ae Remove breadcrumbs 2024-07-20 19:15:01 -04:00
notCharles
8662806dfd Fix 500 if update url is blank 2024-07-20 18:51:38 -04:00
MartinOscar
acf43f2826 Ability to create allocations on EditServer page (#494)
* Ability to create allocation on edit page + Ability to assign allocation to server on creation

* Disable dehydrate for readonly

* set these to false

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-07-20 11:38:34 -04:00
Boy132
dfba8e3993 Command to cleanup docker images (#495)
* add command to cleanup docker images

* automatically cleanup images daily

* fix request

* fix empty check

* run pint
2024-07-20 17:23:03 +02:00
Boy132
56484a2282 Increase guzzle timeout when running tests (#485)
* increase guzzle timeout when running tests

* catch correct exception
2024-07-20 17:18:45 +02:00
MartinOscar
56b4938dc2 Fix #489 (#490)
* Fix #489

* Update app/Filament/Resources/NodeResource/Pages/EditNode.php

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

* Update app/Filament/Resources/NodeResource/Pages/EditNode.php

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

* Update app/Filament/Resources/NodeResource/Pages/EditNode.php

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

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-07-17 16:22:12 -04:00
Boy132
10806d6d6b Fix SQLite foreign keys (#478)
* start migration to fix sqlite foreign keys

* add remaining foreign keys

* add ".sqlite.backup" files to gitignore
2024-07-17 14:43:04 +02:00
Boy132
a04937d698 Fix PORT_FLOOR check and CIDR_MAX_BITS in AssignmentService (#491)
* fix max cidr

* fix port floor
2024-07-17 13:01:13 +02:00
Boy132
8a3d67ada0 Fix update egg from url (#492) 2024-07-17 13:00:54 +02:00
Charles
833ae30e59 Add timeouts (#483)
* Add timeouts

Add Timeouts to github call.

* use config value
2024-07-15 19:09:52 -04:00
Charles
1fdff43ae7 Add Node CPU/Memory Graphs (#459)
* Update Node Stats

Soon TM

* Update

* Make these smaller

* Change graphs

* Remove this.

Didn't work anyways.

* Update Graphs

* Use User TZ and config var

* Fix math

* Change to per thread.
2024-07-14 16:48:14 -04:00
Boy132
bb7c0e0e66 Add "Delete files" task (#470)
* started "delete files" task

* add logic to DeleteFilesService

* add frontend

* make nicer

* move description to right place
2024-07-10 09:25:15 +02:00
Boy132
447e889a4f Fix default timestamp for activity logs (#468)
* fix default timestamp for activity logs

* fix phpstan
2024-07-10 08:36:24 +02:00
Exotical
1c1c8c0cc6 Fix client Activity tab issues; fixes #465 (#466)
* Remove deploy.locations from validator

* Change location data to optional for backwards compat

* Better styling

* Add back comma to follow coding style

* Remove EventServiceProvider from providers file

Fixes duplicated auth messages in the client Activity tab.

* Add null check on $model->actor

Prevents the client Activity tab page from breaking when an authentication attempt has failed.

* Proper type checking on $model->actor

Chose instanceof as it seems to be the best in terms of type safety.

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>

* Revert removal of EventServiceProvider

* Remove subscription of AuthenticationListener

* Remove subscriptions for auth events

* Remove unused import Dispatcher

* Remove unused import AuthenticationListener

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-07-09 21:30:12 -07:00
notCharles
7dad2d0e42 Fix #464 2024-07-07 19:33:25 -04:00
notCharles
212c93c2ba Fix #462 2024-07-05 18:24:07 -04:00
Boy132
7557dc1c8d Restart queue worker when changing email settings (#457) 2024-07-05 16:17:35 +02:00
Boy132
07735464c7 Add contributing guide (#460) 2024-07-05 01:15:45 +02:00
notCharles
8ba15538a9 Fix ToolTip 2024-07-03 16:33:32 -04:00
Charles
c115c6ddf5 Add Update URL to stock eggs 2024-07-03 10:36:34 -04:00
Charles
160ea1ed50 Enable Update URL
Since importing an egg via url was added, we can enable this.
2024-07-03 10:27:57 -04:00
MartinOscar
7164951085 Update EditServer.php (#455) 2024-07-02 13:31:35 -04:00
Charles
40721a2cb8 Fix #452
Prob not the best solution, but it works

Closes: https://github.com/pelican-dev/panel/issues/452
2024-07-02 08:01:17 -04:00
MartinOscar
c464b321dd Update EditProfile.php (#454) 2024-07-02 07:05:00 -04:00
MartinOscar
0f8c27a297 Update ContainerStatus add Starting|Stopping|Default (#449)
* Update ContainerStatus add Starting

* Update ContainerStatus add Stopping

* Update ContainerStatus add Default

* Update Icons, PHPStan

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-06-30 10:13:08 -04:00
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
Charles
dd7a01aa04 Merge pull request #345 from Boy132/show-git-version
Show update info on dashboard & show git commit (when using git)
2024-06-07 20:00:04 -04:00
Boy132
55badb5644 update colors 2024-06-08 00:43:25 +02:00
Boy132
93f059025c show update info on dashboard, show git commit (when using git) 2024-06-08 00:38:46 +02:00
Charles
7be0cd6928 Merge pull request #323 from Boy132/feature/node-sftp-alias
Add alias for node sftp address
2024-06-07 18:04:44 -04:00
Boy132
0156456919 Merge branch 'pelican-dev:main' into feature/node-sftp-alias 2024-06-07 23:49:38 +02:00
Charles
b9d1ce4438 Merge pull request #334 from pelican-dev/issue/297
Better exception handling
2024-06-07 17:46:33 -04:00
Charles
9ce262bf56 Merge pull request #316 from pelican-dev/issue/node-update
Fix Node Updating
2024-06-07 17:44:10 -04:00
notCharles
7ee52affb2 Update token rotation 2024-06-07 17:38:58 -04:00
Charles
93bfe925b9 Merge pull request #333 from pelican-dev/issue/2
Remove unused parameters
2024-06-07 17:32:40 -04:00
Boy132
cc1ac1eba1 Allow importing eggs via url (#344)
* allow importing eggs via url

* refactor

* run pint

* turn back into one button

* fix empty check

* small cleanup

* removed container for tabs

* Update URL function

* Use sys temp

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-06-07 17:31:34 -04:00
Charles
02d24b8a36 Fix the egg variable disaster... (#331)
* Migrations to update existing eggs in db

* Update stock eggs

* Update Eggs on import

* Also update updated versions of eggs that are uploaded

* Redo this..

Tests passed locally.

* Pint & Update replace

* Squash Migrations, simplify logic

* Maybe this way...

* Swap them over to single call

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2024-06-07 16:23:25 -04:00
Charles
16fac3b5c6 Merge pull request #337 from Boy132/fix/schedules-run-every-minute
Fix schedules running every minute
2024-06-07 05:43:56 -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
Lance Pioch
eb99f53d87 Reset this for now 2024-06-07 00:08:41 -04:00
Lance Pioch
643e4168b9 Add required rule separately 2024-06-06 19:39:46 -04:00
Lance Pioch
51cd7a8e81 Remove unused route files 2024-06-06 16:15:35 -04:00
Boy132
91bf38b63d fix schedules running every minute 2024-06-06 15:53:29 +02:00
Charles
e3699f34d8 Merge pull request #336 from Boy132/fix/default-database-path
Use env value instead of config value for database path
2024-06-06 06:09:51 -04:00
Charles
dc3da2dc98 Merge pull request #335 from Boy132/add/mounts-helper-text
Add helper text to mounts on EditServer page
2024-06-06 06:05:27 -04:00
Boy132
d245751c97 use env value instead of config value 2024-06-06 11:59:24 +02:00
Boy132
e0d7a094ab add helper text to mounts 2024-06-06 10:18:05 +02:00
Lance Pioch
3010e3d61e Better default 2024-06-05 23:37:12 -04:00
Lance Pioch
d68e7218a8 Reformat as table 2024-06-05 23:37:09 -04:00
Lance Pioch
a4435a7454 Pint fix 2024-06-05 22:12:53 -04:00
Lance Pioch
df26c4f9f5 Better exception handling 2024-06-05 21:49:09 -04:00
Lance Pioch
6f1de67523 Remove extraneous parameters 2024-06-05 16:03:04 -04:00
Charles
6f009ee126 Remove cli from php
Every workflow run hangs at attempting to add the cli package and adds ~1 min to the workflow.
2024-06-05 14:15:33 -04:00
Boy132
328e159c6b Merge branch 'pelican-dev:main' into feature/node-sftp-alias 2024-06-05 08:47:20 +02:00
Boy132
f9fd426aca change column type to string
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2024-06-05 08:47:11 +02:00
Lance Pioch
6166fac929 Merge pull request #322 from Boy132/fix/make-user-db-test
Replace DB check in MakeUserCommand
2024-06-04 17:33:47 -04:00
Lance Pioch
4bd1070025 Merge pull request #324 from Boy132/patch-1
Remove maxLength from `variable_value` input
2024-06-04 17:30:16 -04:00
Lance Pioch
2d6e30b646 Merge pull request #326 from Boy132/fix/artisan-queue-again
Another call fix in AppSettingsCommand
2024-06-04 17:29:48 -04:00
Lance Pioch
f61c6b9dc2 Merge pull request #327 from Boy132/patch-2
Fix default sqlite database path in setup command
2024-06-04 17:28:45 -04:00
Lance Pioch
5e29737dc5 Merge pull request #328 from Boy132/fix/pelicanignore
Replace `panelignore` with `pelicanignore`
2024-06-04 15:43:09 -04:00
Boy132
d996019204 fix eslint 2024-06-04 17:49:04 +02:00
Boy132
91d8dbd084 replace panelignore with pelicanignore 2024-06-04 17:48:02 +02:00
Boy132
bb03ddda50 listen on all queues 2024-06-04 17:26:19 +02:00
Boy132
1c66681c0e make default sqlite database path relative 2024-06-04 13:26:05 +02:00
Boy132
0728266826 restart queue service if service already exists 2024-06-04 13:14:54 +02:00
Boy132
d81c9faac6 improve prompts 2024-06-04 13:01:52 +02:00
Boy132
cff54f1969 show output when running p:environment:queue-service 2024-06-04 13:01:24 +02:00
Boy132
201563a13b remove maxLength from variable_value input 2024-06-04 11:20:40 +02:00
Boy132
8f2261f6cd add alias for node sftp address 2024-06-04 09:17:36 +02:00
Boy132
29cc92f0dc replace db check in MakeUserCommand 2024-06-04 08:33:54 +02:00
Lance Pioch
33f10cbcb9 Merge pull request #312 from RMartinOscar/patch-1
Update EditUser.php
2024-06-03 10:35:31 -04:00
Lance Pioch
b538532e34 Merge pull request #314 from RMartinOscar/patch-2
Update EditDatabaseHost.php
2024-06-03 10:35:07 -04:00
Lance Pioch
a892821b4f Merge pull request #319 from RMartinOscar/patch-3
Update AllocationsRelationManager to allow big endian
2024-06-03 10:34:27 -04:00
Lance Pioch
5a3b50b31f Apply suggestions from code review 2024-06-03 10:34:08 -04:00
Lance Pioch
51b217571b Merge pull request #320 from Boy132/fix/artisan-call
Fix artisan call in AppSettingsCommand
2024-06-03 10:33:00 -04:00
Boy132
6e75c76c60 cleanup 2024-06-03 13:46:48 +02:00
Boy132
e22c5c3e0a fix artisan call in app settings command 2024-06-03 13:43:11 +02:00
MartinOscar
f3171939a4 Update AllocationsRelationManager.php
Remove useless range order
2024-06-03 07:11:09 +02:00
MartinOscar
189d564f87 Update AllocationsRelationManager.php 2024-06-03 06:30:05 +02:00
MartinOscar
7926f97c8e Update EditDatabaseHost.php 2024-06-03 04:09:36 +02:00
MartinOscar
f4d39c1c68 Update EditDatabaseHost.php 2024-06-03 04:02:31 +02:00
Lance Pioch
6c2d0a2d50 Remove shenanigans 2024-06-02 21:59:12 -04:00
MartinOscar
f6899301fd Update EditDatabaseHost.php 2024-06-03 03:54:33 +02:00
MartinOscar
cbb4ef1da2 Update EditUser.php 2024-06-03 03:52:39 +02:00
notCharles
f6ef76d98e Disable delete for own user. 2024-06-02 21:00:11 -04:00
notCharles
65a697d8f7 add variables to .env 2024-06-02 18:04:02 -04:00
Charles
9515a82a75 Merge pull request #280 from pelican-dev/charles/rework-server
Rework Server Pages
2024-06-02 17:54:00 -04:00
Lance Pioch
44f5ea567f Merge branch 'main' into charles/rework-server
# Conflicts:
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
2024-06-02 17:46:45 -04:00
Charles
88f910f3e7 Merge pull request #307 from pelican-dev/issue/287
Allow Servers to have Mounts
2024-06-02 17:42:13 -04:00
Lance Pioch
020f028008 Add new component 2024-06-02 17:38:07 -04:00
Lance Pioch
0cb7f737b0 Set the node 2024-06-02 17:38:00 -04:00
notCharles
53aa52f519 Add migration to update stock egg uuids 2024-06-02 17:06:42 -04:00
notCharles
e884eda5a7 Update stock eggs to have UUIDs 2024-06-02 16:50:55 -04:00
notCharles
58d1fd3917 Add Mounts + Icons 2024-06-02 15:05:45 -04:00
notCharles
0b0952650e Remove labels/mounts if empty. 2024-06-02 13:43:25 -04:00
notCharles
aa55a7ed83 Update .gitignore, again... 2024-06-02 10:51:48 -04:00
notCharles
c7fa7a1bad Add to .gitignore 2024-06-02 10:48:45 -04:00
Lance Pioch
4a3bdd78ef Update readme 2024-06-02 00:38:54 -04:00
Lance Pioch
a1067fd4aa Allow mounts to be added to servers 2024-06-02 00:34:35 -04:00
Lance Pioch
110cc1248b Fix relationship 2024-06-02 00:33:58 -04:00
Lance Pioch
04a1ccc97e Merge branch 'main' of github.com:pelican-dev/panel 2024-06-01 19:26:25 -04:00
Lance Pioch
5e7f5c2a4c Allow adding new allocations to existing servers 2024-06-01 19:26:23 -04:00
kubi
b804878d7b Fix labels position in server config 2024-06-01 20:42:44 +00:00
notCharles
118977c8c5 Merge branch 'main' into charles/rework-server 2024-06-01 15:54:03 -04:00
notCharles
c31b7b8c6a Correctly save labels on create 2024-06-01 15:52:13 -04:00
Charles
eefe59b153 Merge pull request #300 from pelican-dev/issue/286
2FA Profile
2024-06-01 13:07:32 -04:00
Lance Pioch
cd4b7cbf9e Refresh this 2024-06-01 13:03:46 -04:00
Lance Pioch
67cb3d4816 Show backup tokens better 2024-06-01 12:49:36 -04:00
Lance Pioch
7762e68a6c Make qr code visible on light mode 2024-06-01 12:49:19 -04:00
Lance Pioch
7a327ea378 Remove clockwork 2024-06-01 12:36:11 -04:00
Lance Pioch
b3ca7b7ac9 Merge pull request #284 from Boy132/replace/encrypt-decrypt
Replace encrypt/decrypt with `encrypted` casting to resolve #4
2024-05-31 19:50:07 -04:00
Charles
abc99cd928 Merge pull request #303 from pelican-dev/issue/290
Allow updating of existing eggs via upload
2024-05-31 18:52:20 -04:00
Charles
cb638369cf Merge pull request #302 from pelican-dev/issue/294
Only show application api keys
2024-05-31 18:49:35 -04:00
notCharles
9174de2d8c Add Labels 2024-05-31 17:24:03 -04:00
Boy132
7cda358b66 add missing import 2024-05-31 23:07:50 +02:00
Boy132
33f6551b21 run pint 2024-05-31 23:06:46 +02:00
Boy132
b1928e89b4 Merge branch 'pelican-dev:main' into replace/encrypt-decrypt 2024-05-31 23:04:43 +02:00
Lance Pioch
c956cd0106 Update old keys 2024-05-31 17:03:14 -04:00
notCharles
5081cc3f63 Fix badge, update table 2024-05-31 16:39:23 -04:00
Lance Pioch
8eb2c23420 Fix creating new node 2024-05-31 16:03:12 -04:00
notCharles
cfe385f53a Add uuid to egg exproter
Add UUID to egg exporter.
2024-05-31 16:01:15 -04:00
Lance Pioch
264d3498a6 Allow mailgun to work #257 2024-05-31 15:44:21 -04:00
Lance Pioch
065f3f2468 Activity log fix #257 2024-05-31 15:18:04 -04:00
Lance Pioch
957638d4ac Fill previously existing egg 2024-05-31 15:14:22 -04:00
Lance Pioch
7d0ce1627b Remove unused imports 2024-05-31 02:00:38 -04:00
Lance Pioch
8cec7368ab Only show application api keys 2024-05-31 01:59:33 -04:00
Lance Pioch
5519931ee5 Pint 2024-05-31 01:42:02 -04:00
Charles
97ac0fe54b Add Reset Daemon Key Button (#298) closes #292 2024-05-31 01:41:21 -04:00
Lance Pioch
7657364208 Cache per user and show backup tokens temporarily 2024-05-31 01:38:32 -04:00
Lance Pioch
ef1a208b95 Add 2fa setup 2024-05-31 01:26:28 -04:00
Lance Pioch
aa82c6dd04 Update this 2024-05-31 01:20:25 -04:00
Lance Pioch
8ecabef6b5 Add customization 2024-05-29 19:24:02 -04:00
notCharles
a6d07ede5a Soon-TM 2024-05-29 19:18:09 -04:00
Boy132
f6325c07c4 Fix overallocation -1 and close #268 (#283) 2024-05-29 18:57:30 -04:00
Exotical
7674ee0e2b Make deploy.locations optional in the api (#295) 2024-05-29 18:54:07 -04:00
Senna
5760e72b8f Added 2 badges (#296) 2024-05-29 18:51:45 -04:00
Boy132
b6e46f758d Remove hashids (#282) and close #269 2024-05-29 18:41:44 -04:00
notCharles
e980877bbc Fix Node Creation
Add missing defaults
2024-05-29 18:28:21 -04:00
notCharles
dd223b47c0 WIP Server Transfer 2024-05-29 18:27:54 -04:00
Boy132
639fa3399d run pint 2024-05-28 15:27:33 +02:00
Boy132
82fd547484 replace encrypt/ decrypt with encrypted casting 2024-05-28 15:24:20 +02:00
notCharles
d461242f08 Improve Logic on buttons
If a server is suspended, disable transfer/toggle/reinstall as they will unsuspend the server due to the status change.

Also properly updates server state and container status.
2024-05-27 21:51:24 -04:00
notCharles
dec1cf8e74 Rework Edit Server Page
a WIP, Also functional
2024-05-27 20:02:13 -04:00
notCharles
15caac51fb fix auth redirect
closes https://github.com/pelican-dev/panel/issues/278
2024-05-27 13:41:46 -04:00
notCharles
183c274a0d Correct Tags to KeyVal 2024-05-27 13:37:59 -04:00
Lance Pioch
a8b2fb440f Merge branch 'main' of github.com:pelican-dev/panel 2024-05-25 20:59:54 -04:00
Lance Pioch
f8e4514998 Update filename 2024-05-25 20:51:52 -04:00
Lance Pioch
deeebf73d3 Update gitignores 2024-05-25 20:51:41 -04:00
Boy132
422fc102c9 Improve "no interaction" mode for queue worker service command (#270) 2024-05-25 20:48:02 -04:00
Boy132
e715e92f9d correctly transform eggs that use inheritance (application api) (#217) 2024-05-25 20:47:27 -04:00
Lance Pioch
73babfa2b3 Merge pull request #274 from pelican-dev/issue/267 and fix #267 2024-05-24 21:15:14 -04:00
Lance Pioch
e0a92d733b I swear I already did this 2024-05-24 20:58:19 -04:00
Jordan Adams
1e67cd9944 Fix mumble host to allow IPv6 (#264) 2024-05-24 19:36:18 -04:00
Lance Pioch
3946116dff Merge pull request #265 from pelican-dev/issue/222
Simplify node deployment service, add filtering with tags instead of locations
2024-05-24 19:35:06 -04:00
Lance Pioch
b77fd3d653 Fix #267 2024-05-24 19:34:23 -04:00
Lance Pioch
f4672c6cb1 Pint fix 2024-05-22 03:15:29 -04:00
Lance Pioch
5b9e4b1729 Always limit by nodes, was like this before anyways 2024-05-22 03:10:33 -04:00
Lance Pioch
48f715ae69 Fix directory 2024-05-22 02:52:49 -04:00
Lance Pioch
51460782cc Merge branch 'main' into issue/222
# Conflicts:
#	app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php
#	app/Http/Requests/Api/Application/Nodes/GetDeployableNodesRequest.php
#	app/Services/Deployment/FindViableNodesService.php
#	app/Services/Servers/ServerCreationService.php
#	tests/Integration/Services/Deployment/FindViableNodesServiceTest.php
2024-05-22 02:47:37 -04:00
Lance Pioch
b007e63937 Pint fixes 2024-05-22 02:35:20 -04:00
Boy132
4dd833562b Add CPU limit to node (#239) to resolve #233
* add node cpu limit to backend

* update makenodecommand

* add node cpu limit to frontend

* add migration and update mysql schema

* run pint

* fix typo in mysql schema

* forgot this assert

* forgot to setCpu here

* run pint

* adjust migration

* Fix db migration

* make cpu optional

* set default value for cpu in node deployment

* update mysql schema

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-05-22 02:34:43 -04:00
Lance Pioch
b579f14f3f Backwards compatibility 2024-05-21 22:04:12 -04:00
Lance Pioch
eadaec1b30 Simplify now that keys are fixed 2024-05-21 21:48:16 -04:00
Lance Pioch
a9e58bb493 Fix database path for sqlite 2024-05-21 21:48:04 -04:00
Lance Pioch
5c33c7495a Ignore this for now 2024-05-21 21:45:26 -04:00
Lance Pioch
f9aa8cf218 Simplify viable nodes service 2024-05-21 21:44:49 -04:00
Lance Pioch
da698a3666 Remove exception 2024-05-21 21:02:11 -04:00
Lance Pioch
2808a3dd35 Simplify buttons 2024-05-20 14:38:48 -04:00
Lance Pioch
7ea365e8de Pint 2024-05-19 23:25:40 -04:00
Lance Pioch
ae399f9bad Add port validation rule for #68 2024-05-19 23:25:12 -04:00
Lance Pioch
53a5ff6e6d Update api docs 2024-05-19 23:24:21 -04:00
Lance Pioch
54ae4b3dc1 Merge pull request #261 from pelican-dev/charles/docker-tags
Add docker container labels
2024-05-19 21:36:26 -04:00
notCharles
859a721e17 mysql vs sqlite... 2024-05-19 21:30:25 -04:00
notCharles
03cbdd5bdd update edit/create pages 2024-05-19 21:15:43 -04:00
notCharles
4c43fd1683 Add docker_labels 2024-05-19 20:55:37 -04:00
notCharles
0c61a63191 Add id to allocation table 2024-05-19 20:23:59 -04:00
Boy132
b1f99ca8a3 Add api for mounts (#160)
* add application api endpoints for mounts

* run pint

* add mounts resource to api key

* add includes to mount transformer

* forgot delete route for mount itself

* add migration for "r_mounts" column

* add mounts to testcase api key
2024-05-19 08:50:15 -07:00
notCharles
0a5810358a Update jdbc string
should also update the password here.
2024-05-18 18:17:20 -04:00
notCharles
1bae239971 Fix db password rotation
updates the password textbox when password is rotated.
2024-05-18 18:13:06 -04:00
notCharles
597f74f105 reload form data after save
closes https://github.com/pelican-dev/panel/issues/251
2024-05-18 18:08:55 -04:00
notCharles
5344d99a40 Update Mobile View 2024-05-18 17:47:33 -04:00
Boy132
1db1a1a3e0 set default db username to "pelican" to match docs (#254) 2024-05-18 12:38:11 -07:00
Boy132
712b6a285b Add artisan command to create queue worker service (#253)
* add command to create queue worker service file

* remove comments from service file that are no longer needed

* only create queue worker service file if queue driver is not "sync"

* make "database" the recommended queue driver, again
2024-05-18 10:31:02 -07:00
notCharles
38b92ae21d Fix user->admin
closes https://github.com/pelican-dev/panel/issues/197
2024-05-17 22:58:17 -04:00
292 changed files with 8842 additions and 5118 deletions

View File

@@ -4,7 +4,7 @@ APP_KEY=
APP_TIMEZONE=UTC
APP_URL=http://panel.test
APP_LOCALE=en
APP_ENVIRONMENT_ONLY=true
APP_INSTALLED=false
LOG_CHANNEL=daily
LOG_STACK=single
@@ -17,9 +17,6 @@ CACHE_STORE=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
HASHIDS_SALT=
HASHIDS_LENGTH=8
MAIL_MAILER=log
MAIL_HOST=smtp.example.com
MAIL_PORT=25
@@ -30,6 +27,7 @@ MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="Pelican Admin"
# Set this to your domain to prevent it defaulting to 'localhost', causing mail servers such as Gmail to reject your mail
# MAIL_EHLO_DOMAIN=panel.example.com
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

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

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

65
.gitignore vendored
View File

@@ -1,41 +1,28 @@
/vendor
*.DS_Store*
!.env.ci
!.env.example
.env*
.vagrant/*
.vscode/*
storage/framework/*
/.idea
/nbproject
/.direnv
node_modules
*.log
_ide_helper.php
_ide_helper_models.php
.phpstorm.meta.php
.yarn
public/assets/manifest.json
*.sqlite
# For local development with docker
# Remove if we ever put the Dockerfile in the repo
.dockerignore
docker-compose.yml
# for image related files
misc
.php-cs-fixer.cache
coverage.xml
resources/lang/locales.js
.phpunit.result.cache
/.phpunit.cache
/node_modules
/public/build
/public/hot
result
docker-compose.yaml
public/css/filament-monaco-editor/
public/js/filament-monaco-editor/
/public/storage
/storage/*.key
/storage/clockwork/*
/vendor
*.DS_Store*
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
public/assets/manifest.json
/database/*.sqlite
filament-monaco-editor/
_ide_helper*
/.phpstorm.meta.php

View File

@@ -3,7 +3,6 @@
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use Illuminate\Support\Facades\Artisan;
@@ -11,156 +10,41 @@ class AppSettingsCommand extends Command
{
use EnvironmentWriterTrait;
public const CACHE_DRIVERS = [
'file' => 'Filesystem (recommended)',
'redis' => 'Redis',
];
public const SESSION_DRIVERS = [
'file' => 'Filesystem (recommended)',
'redis' => 'Redis',
'database' => 'Database',
'cookie' => 'Cookie',
];
public const QUEUE_DRIVERS = [
'sync' => 'Synchronous (recommended)',
'database' => 'Database',
'redis' => 'Redis',
];
protected $description = 'Configure basic environment settings for the Panel.';
protected $signature = 'p:environment:setup
{--new-salt : Whether or not to generate a new salt for Hashids.}
{--url= : The URL that this Panel is running on.}
{--cache= : The cache driver backend to use.}
{--session= : The session driver backend to use.}
{--queue= : The queue driver backend to use.}
{--redis-host= : Redis host to use for connections.}
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}
{--settings-ui= : Enable or disable the settings UI.}';
{--url= : The URL that this Panel is running on.}';
protected array $variables = [];
/**
* AppSettingsCommand constructor.
*/
public function __construct(private Kernel $console)
public function handle(): void
{
parent::__construct();
}
/**
* Handle command execution.
*
* @throws \App\Exceptions\PanelException
*/
public function handle(): int
{
$this->variables['APP_TIMEZONE'] = 'UTC';
if (empty(config('hashids.salt')) || $this->option('new-salt')) {
$this->variables['HASHIDS_SALT'] = str_random(20);
$path = base_path('.env');
if (!file_exists($path)) {
$this->comment('Copying example .env file');
copy($path . '.example', $path);
}
$this->output->comment(__('commands.appsettings.comment.url'));
if (!config('app.key')) {
$this->comment('Generating app key');
Artisan::call('key:generate');
}
$this->variables['APP_TIMEZONE'] = 'UTC';
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
'Application URL',
config('app.url', 'https://example.com')
);
$selected = config('cache.default', 'file');
$this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice(
'Cache Driver',
self::CACHE_DRIVERS,
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
);
$selected = config('session.driver', 'file');
$this->variables['SESSION_DRIVER'] = $this->option('session') ?? $this->choice(
'Session Driver',
self::SESSION_DRIVERS,
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
);
$selected = config('queue.default', 'database');
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
'Queue Driver',
self::QUEUE_DRIVERS,
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
);
if (!is_null($this->option('settings-ui'))) {
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true';
} else {
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true';
}
// Make sure session cookies are set as "secure" when using HTTPS
if (str_starts_with($this->variables['APP_URL'], 'https://')) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
}
$this->checkForRedis();
$path = base_path('.env');
if (!file_exists($path)) {
copy($path . '.example', $path);
}
$this->comment('Writing variables to .env file');
$this->writeToEnvironment($this->variables);
if (!config('app.key')) {
Artisan::call('key:generate');
}
$this->info($this->console->output());
return 0;
}
/**
* Check if redis is selected, if so, request connection details and verify them.
*/
private function checkForRedis()
{
$items = collect($this->variables)->filter(function ($item) {
return $item === 'redis';
});
// Redis was not selected, no need to continue.
if (count($items) === 0) {
return;
}
$this->output->note(__('commands.appsettings.redis.note'));
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
'Redis Host',
config('database.redis.default.host')
);
$askForRedisPassword = true;
if (!empty(config('database.redis.default.password'))) {
$this->variables['REDIS_PASSWORD'] = config('database.redis.default.password');
$askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?');
}
if ($askForRedisPassword) {
$this->output->comment(__('commands.appsettings.redis.comment'));
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
'Redis Password'
);
}
if (empty($this->variables['REDIS_PASSWORD'])) {
$this->variables['REDIS_PASSWORD'] = 'null';
}
$this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask(
'Redis Port',
config('database.redis.default.port')
);
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use App\Traits\Commands\RequestRedisSettingsTrait;
class CacheSettingsCommand extends Command
{
use EnvironmentWriterTrait;
use RequestRedisSettingsTrait;
public const CACHE_DRIVERS = [
'file' => 'Filesystem (default)',
'database' => 'Database',
'redis' => 'Redis',
];
protected $description = 'Configure cache settings for the Panel.';
protected $signature = 'p:environment:cache
{--driver= : The cache driver backend to use.}
{--redis-host= : Redis host to use for connections.}
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/**
* CacheSettingsCommand constructor.
*/
public function __construct(private Kernel $console)
{
parent::__construct();
}
/**
* Handle command execution.
*/
public function handle(): int
{
$selected = config('cache.default', 'file');
$this->variables['CACHE_STORE'] = $this->option('driver') ?? $this->choice(
'Cache Driver',
self::CACHE_DRIVERS,
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
);
if ($this->variables['CACHE_STORE'] === 'redis') {
$this->requestRedisSettings();
if (config('queue.default') !== 'sync') {
$this->call('p:environment:queue-service', [
'--use-redis' => true,
'--overwrite' => true,
]);
}
}
$this->writeToEnvironment($this->variables);
$this->info($this->console->output());
return 0;
}
}

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,12 +107,72 @@ class DatabaseSettingsCommand extends Command
return $this->handle();
}
return 1;
}
} elseif ($this->variables['DB_CONNECTION'] === 'mariadb') {
$this->output->note(__('commands.database_settings.DB_HOST_note'));
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
'Database Host',
config('database.connections.mariadb.host', '127.0.0.1')
);
$this->variables['DB_PORT'] = $this->option('port') ?? $this->ask(
'Database Port',
config('database.connections.mariadb.port', 3306)
);
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
'Database Name',
config('database.connections.mariadb.database', 'panel')
);
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
'Database Username',
config('database.connections.mariadb.username', 'pelican')
);
$askForMariaDBPassword = true;
if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) {
$this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password');
$askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
}
if ($askForMariaDBPassword) {
$this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password');
}
try {
// Test connection
config()->set('database.connections._panel_command_test', [
'driver' => 'mariadb',
'host' => $this->variables['DB_HOST'],
'port' => $this->variables['DB_PORT'],
'database' => $this->variables['DB_DATABASE'],
'username' => $this->variables['DB_USERNAME'],
'password' => $this->variables['DB_PASSWORD'],
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
$this->database->connection('_panel_command_test')->getPdo();
} catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(__('commands.database_settings.DB_error_2'));
if ($this->confirm(__('commands.database_settings.go_back'))) {
$this->database->disconnect('_panel_command_test');
return $this->handle();
}
return 1;
}
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
'Database Path',
config('database.connections.sqlite.database', database_path('database.sqlite'))
env('DB_DATABASE', 'database.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

@@ -61,6 +61,8 @@ class EmailSettingsCommand extends Command
$this->writeToEnvironment($this->variables);
$this->call('queue:restart');
$this->line('Updating stored environment configuration file.');
$this->line('');
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use App\Traits\Commands\RequestRedisSettingsTrait;
class QueueSettingsCommand extends Command
{
use EnvironmentWriterTrait;
use RequestRedisSettingsTrait;
public const QUEUE_DRIVERS = [
'database' => 'Database (default)',
'redis' => 'Redis',
'sync' => 'Synchronous',
];
protected $description = 'Configure queue settings for the Panel.';
protected $signature = 'p:environment:queue
{--driver= : The queue driver backend to use.}
{--redis-host= : Redis host to use for connections.}
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/**
* QueueSettingsCommand constructor.
*/
public function __construct(private Kernel $console)
{
parent::__construct();
}
/**
* Handle command execution.
*/
public function handle(): int
{
$selected = config('queue.default', 'database');
$this->variables['QUEUE_CONNECTION'] = $this->option('driver') ?? $this->choice(
'Queue Driver',
self::QUEUE_DRIVERS,
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
);
if ($this->variables['QUEUE_CONNECTION'] === 'redis') {
$this->requestRedisSettings();
$this->call('p:environment:queue-service', [
'--use-redis' => true,
'--overwrite' => true,
]);
}
$this->writeToEnvironment($this->variables);
$this->info($this->console->output());
return 0;
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
class QueueWorkerServiceCommand extends Command
{
protected $description = 'Create the service for the queue worker.';
protected $signature = 'p:environment:queue-service
{--service-name= : Name of the queue worker service.}
{--user= : The user that PHP runs under.}
{--group= : The group that PHP runs under.}
{--overwrite : Force overwrite if the service file already exists.}';
public function handle(): void
{
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service';
$fileExists = file_exists($path);
if ($fileExists && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
$this->line('Creation of queue worker service file aborted because service file already exists.');
return;
}
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
$redisUsed = config('queue.default') === 'redis' || config('session.driver') === 'redis' || config('cache.default') === 'redis';
$afterRedis = $redisUsed ? '
After=redis-server.service' : '';
$basePath = base_path();
$success = File::put($path, "# Pelican Queue File
# ----------------------------------
[Unit]
Description=Pelican Queue Service$afterRedis
[Service]
User=$user
Group=$group
Restart=always
ExecStart=/usr/bin/php $basePath/artisan queue:work --tries=3
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
");
if (!$success) {
$this->error('Error creating service file');
return;
}
if ($fileExists) {
$result = Process::run("systemctl restart $serviceName.service");
if ($result->failed()) {
$this->error('Error restarting service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file updated successfully.');
} else {
$result = Process::run("systemctl enable --now $serviceName.service");
if ($result->failed()) {
$this->error('Error enabling service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file created successfully.');
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use App\Traits\Commands\RequestRedisSettingsTrait;
class SessionSettingsCommand extends Command
{
use EnvironmentWriterTrait;
use RequestRedisSettingsTrait;
public const SESSION_DRIVERS = [
'file' => 'Filesystem (default)',
'redis' => 'Redis',
'database' => 'Database',
'cookie' => 'Cookie',
];
protected $description = 'Configure session settings for the Panel.';
protected $signature = 'p:environment:session
{--driver= : The session driver backend to use.}
{--redis-host= : Redis host to use for connections.}
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/**
* SessionSettingsCommand constructor.
*/
public function __construct(private Kernel $console)
{
parent::__construct();
}
/**
* Handle command execution.
*/
public function handle(): int
{
$selected = config('session.driver', 'file');
$this->variables['SESSION_DRIVER'] = $this->option('driver') ?? $this->choice(
'Session Driver',
self::SESSION_DRIVERS,
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
);
if ($this->variables['SESSION_DRIVER'] === 'redis') {
$this->requestRedisSettings();
if (config('queue.default') !== 'sync') {
$this->call('p:environment:queue-service', [
'--use-redis' => true,
'--overwrite' => true,
]);
}
}
$this->writeToEnvironment($this->variables);
$this->info($this->console->output());
return 0;
}
}

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)
{
@@ -26,45 +26,76 @@ class InfoCommand extends Command
{
$this->output->title('Version Information');
$this->table([], [
['Panel Version', config('app.version')],
['Panel Version', $this->versionService->versionData()['version']],
['Latest Version', $this->versionService->getPanel()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
], 'compact');
$this->output->title('Application Configuration');
$this->table([], [
['Environment', $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

@@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands\Maintenance;
use App\Models\Node;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class PruneImagesCommand extends Command
{
protected $signature = 'p:maintenance:prune-images {node?}';
protected $description = 'Clean up all dangling docker images to clear up disk space.';
public function handle(): void
{
$node = $this->argument('node');
if (empty($node)) {
$nodes = Node::all();
/** @var Node $node */
foreach ($nodes as $node) {
$this->cleanupImages($node);
}
} else {
$this->cleanupImages((int) $node);
}
}
private function cleanupImages(int|Node $node): void
{
if (!$node instanceof Node) {
$node = Node::query()->findOrFail($node);
}
try {
$response = Http::daemon($node)
->connectTimeout(5)
->timeout(30)
->delete('/api/system/docker/image/prune')
->json() ?? [];
if (empty($response) || $response['ImagesDeleted'] === null) {
$this->warn("Node {$node->id}: No images to clean up.");
return;
}
$count = count($response['ImagesDeleted']);
$useBinaryPrefix = config('panel.use_binary_prefix');
$space = round($useBinaryPrefix ? $response['SpaceReclaimed'] / 1024 / 1024 : $response['SpaceReclaimed'] / 1000 / 1000, 2) . ($useBinaryPrefix ? ' MiB' : ' MB');
$this->info("Node {$node->id}: Cleaned up {$count} dangling docker images. ({$space})");
} catch (Exception $exception) {
$this->error($exception->getMessage());
}
}
}

View File

@@ -20,9 +20,12 @@ class MakeNodeCommand extends Command
{--overallocateMemory= : Enter the amount of ram to overallocate (% or -1 to overallocate the maximum).}
{--maxDisk= : Set the max disk amount.}
{--overallocateDisk= : Enter the amount of disk to overallocate (% or -1 to overallocate the maximum).}
{--maxCpu= : Set the max cpu amount.}
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
{--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}';
protected $description = 'Creates a new node on the system via the CLI.';
@@ -54,16 +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['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()->toDateString())
->where('next_run_at', '<=', Carbon::now()->toDateTimeString())
->get();
if ($schedules->count() < 1) {
@@ -62,7 +62,7 @@ class ProcessRunnableCommand extends Command
$this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'hash' => $schedule->hashid,
'id' => $schedule->id,
]));
} catch (\Throwable|\Exception $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]);

View File

@@ -15,7 +15,7 @@ class DeleteUserCommand extends Command
public function handle(): int
{
$search = $this->option('user') ?? $this->ask(trans('command/messages.user.search_users'));
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
Assert::notEmpty($search, 'Search term should not be empty.');
$results = User::query()
->where('id', 'LIKE', "$search%")
@@ -42,6 +42,8 @@ class DeleteUserCommand extends Command
if (!$deleteUser = $this->ask(trans('command/messages.user.select_search_user'))) {
return $this->handle();
}
$deleteUser = User::query()->findOrFail($deleteUser);
} else {
if (count($results) > 1) {
$this->error(trans('command/messages.user.multiple_found'));
@@ -53,8 +55,7 @@ class DeleteUserCommand extends Command
}
if ($this->confirm(trans('command/messages.user.confirm_delete')) || !$this->input->isInteractive()) {
$user = User::query()->findOrFail($deleteUser);
$user->delete();
$deleteUser->delete();
$this->info(trans('command/messages.user.deleted'));
}

View File

@@ -30,7 +30,7 @@ class MakeUserCommand extends Command
public function handle(): int
{
try {
DB::select('select 1 where 1');
DB::connection()->getPdo();
} catch (Exception $exception) {
$this->error($exception->getMessage());

View File

@@ -2,6 +2,7 @@
namespace App\Console;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand;
@@ -9,6 +10,7 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand;
class Kernel extends ConsoleKernel
{
@@ -30,7 +32,11 @@ class Kernel extends ConsoleKernel
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
$schedule->command(PruneImagesCommand::class)->daily();
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.

View File

@@ -1,15 +0,0 @@
<?php
namespace App\Contracts\Extensions;
use Hashids\HashidsInterface as VendorHashidsInterface;
interface HashidsInterface extends VendorHashidsInterface
{
/**
* Decode an encoded hashid and return the first result.
*
* @throws \InvalidArgumentException
*/
public function decodeFirst(string $encoded, string $default = null): mixed;
}

View File

@@ -6,12 +6,15 @@ enum ContainerStatus: string
{
// Docker Based
case Created = 'created';
case Starting = 'starting';
case Running = 'running';
case Restarting = 'restarting';
case Exited = 'exited';
case Paused = 'paused';
case Dead = 'dead';
case Removing = 'removing';
case Stopping = 'stopping';
case Offline = 'offline';
// HTTP Based
case Missing = 'missing';
@@ -19,14 +22,17 @@ enum ContainerStatus: string
public function icon(): string
{
return match ($this) {
self::Created => 'tabler-heart-plus',
self::Starting => 'tabler-heart-up',
self::Running => 'tabler-heartbeat',
self::Restarting => 'tabler-heart-bolt',
self::Exited => 'tabler-heart-exclamation',
self::Paused => 'tabler-heart-pause',
self::Dead => 'tabler-heart-x',
self::Dead, self::Offline => 'tabler-heart-x',
self::Removing => 'tabler-heart-down',
self::Missing => 'tabler-heart-question',
self::Missing => 'tabler-heart-search',
self::Stopping => 'tabler-heart-minus',
};
}
@@ -34,6 +40,7 @@ enum ContainerStatus: string
{
return match ($this) {
self::Created => 'primary',
self::Starting => 'warning',
self::Running => 'success',
self::Restarting => 'info',
self::Exited => 'danger',
@@ -41,6 +48,8 @@ enum ContainerStatus: string
self::Dead => 'danger',
self::Removing => 'warning',
self::Missing => 'danger',
self::Stopping => 'warning',
self::Offline => 'gray',
};
}
}

View File

@@ -48,7 +48,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
*/
public function render(Request $request)
{
if (str($request->url())->contains('livewire')) {
if ($request->is('livewire/update')) {
Notification::make()
->title(static::class)
->body($this->getMessage())

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,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableNodeException extends DisplayException
{
}

View File

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

View File

@@ -6,9 +6,9 @@ use App\Exceptions\DisplayException;
class TwoFactorAuthenticationTokenInvalid extends DisplayException
{
/**
* TwoFactorAuthenticationTokenInvalid constructor.
*/
public string $title = 'Invalid 2FA Code';
public string $icon = 'tabler-2fa';
public function __construct()
{
parent::__construct('The provided two-factor authentication token was not valid.');

View File

@@ -25,7 +25,7 @@ class DynamicDatabaseConnection
'port' => $host->port,
'database' => $database,
'username' => $host->username,
'password' => decrypt($host->password),
'password' => $host->password,
'charset' => self::DB_CHARSET,
'collation' => self::DB_COLLATION,
]);

View File

@@ -1,22 +0,0 @@
<?php
namespace App\Extensions;
use Hashids\Hashids as VendorHashids;
use App\Contracts\Extensions\HashidsInterface;
class Hashids extends VendorHashids implements HashidsInterface
{
/**
* {@inheritdoc}
*/
public function decodeFirst(string $encoded, string $default = null): mixed
{
$result = $this->decode($encoded);
if (!is_array($result)) {
return $default;
}
return array_first($result, null, $default);
}
}

View File

@@ -1,10 +0,0 @@
<?php
namespace App\Filament\Clusters;
use Filament\Clusters\Cluster;
class Settings extends Cluster
{
protected static ?string $navigationIcon = 'tabler-settings';
}

View File

@@ -7,6 +7,7 @@ use App\Models\Egg;
use App\Models\Node;
use App\Models\Server;
use App\Models\User;
use App\Services\Helpers\SoftwareVersionService;
use Filament\Actions\CreateAction;
use Filament\Pages\Page;
@@ -29,8 +30,14 @@ class Dashboard extends Page
public function getViewData(): array
{
/** @var SoftwareVersionService $softwareVersionService */
$softwareVersionService = app(SoftwareVersionService::class);
return [
'inDevelopment' => config('app.version') === 'canary',
'version' => $softwareVersionService->versionData()['version'],
'latestVersion' => $softwareVersionService->getPanel(),
'isLatest' => $softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
'nodesCount' => Node::query()->count(),
@@ -39,15 +46,17 @@ class Dashboard extends Page
'devActions' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-developers.button_issues'))
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/issues/new/choose', true)
->color('warning'),
CreateAction::make()
->label(trans('dashboard/index.sections.intro-developers.button_features'))
->label('Bugs & Features')
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/discussions', true),
],
'updateActions' => [
CreateAction::make()
->label('Read Documentation')
->icon('tabler-clipboard-text')
->url('https://pelican.dev/docs/panel/update', true)
->color('warning'),
],
'nodeActions' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
@@ -55,14 +64,10 @@ class Dashboard extends Page
->url(route('filament.admin.resources.nodes.create')),
],
'supportActions' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_translate'))
->icon('tabler-language')
->url('https://crowdin.com/project/pelican-dev', true),
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url('https://pelican.dev/donate', true)
->url($softwareVersionService->getDonations(), true)
->color('success'),
],
'helpActions' => [
@@ -70,11 +75,6 @@ class Dashboard extends Page
->label(trans('dashboard/index.sections.intro-help.button_docs'))
->icon('tabler-speedboat')
->url('https://pelican.dev/docs', true),
CreateAction::make()
->label(trans('dashboard/index.sections.intro-help.button_discord'))
->icon('tabler-brand-discord')
->url('https://discord.gg/pelican-panel', true)
->color('blurple'),
],
];
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Filament\Pages\Installer;
use App\Filament\Pages\Installer\Steps\AdminUserStep;
use App\Filament\Pages\Installer\Steps\DatabaseStep;
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
use App\Filament\Pages\Installer\Steps\RedisStep;
use App\Filament\Pages\Installer\Steps\RequirementsStep;
use App\Services\Users\UserCreationService;
use App\Traits\Commands\EnvironmentWriterTrait;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Wizard;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\SimplePage;
use Filament\Support\Enums\MaxWidth;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
/**
* @property Form $form
*/
class PanelInstaller extends SimplePage implements HasForms
{
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;
public $data = [];
protected static string $view = 'filament.pages.installer';
public function getMaxWidth(): MaxWidth|string
{
return MaxWidth::SevenExtraLarge;
}
public function mount()
{
if (is_installed()) {
abort(404);
}
$this->form->fill();
}
public function dehydrate(): void
{
Artisan::call('config:clear');
Artisan::call('cache:clear');
}
protected function getFormSchema(): array
{
return [
Wizard::make([
RequirementsStep::make(),
EnvironmentStep::make(),
DatabaseStep::make(),
RedisStep::make()
->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'),
AdminUserStep::make(),
])
->persistStepInQueryString()
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
type="submit"
size="sm"
>
Finish
</x-filament::button>
BLADE))),
];
}
protected function getFormStatePath(): ?string
{
return 'data';
}
protected function hasUnsavedDataChangesAlert(): bool
{
return true;
}
public function submit()
{
try {
$inputs = $this->form->getState();
// Write variables to .env file
$variables = array_get($inputs, 'env');
$this->writeToEnvironment($variables);
// Run migrations
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
]);
// Create first admin user
$userData = array_get($inputs, 'user');
$userData['root_admin'] = true;
app(UserCreationService::class)->handle($userData);
// Install setup complete
$this->writeToEnvironment(['APP_INSTALLED' => true]);
$this->rememberData();
Notification::make()
->title('Successfully Installed')
->success()
->send();
redirect()->intended(Filament::getUrl());
} catch (Exception $exception) {
Notification::make()
->title('Installation Failed')
->body($exception->getMessage())
->danger()
->send();
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
class AdminUserStep
{
public static function make(): Step
{
return Step::make('user')
->label('Admin User')
->schema([
TextInput::make('user.email')
->label('Admin E-Mail')
->required()
->email()
->default('admin@example.com'),
TextInput::make('user.username')
->label('Admin Username')
->required()
->default('admin'),
TextInput::make('user.password')
->label('Admin Password')
->required()
->password()
->revealable(),
]);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\DatabaseManager;
use PDOException;
class DatabaseStep
{
public static function make(): Step
{
return Step::make('database')
->label('Database')
->columns()
->schema([
TextInput::make('env.DB_DATABASE')
->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
->columnSpanFull()
->hintIcon('tabler-question-mark')
->hintIconTooltip(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
->required()
->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
TextInput::make('env.DB_HOST')
->label('Database Host')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your database. Make sure it is reachable.')
->required()
->default(env('DB_HOST', '127.0.0.1'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_PORT')
->label('Database Port')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your database.')
->required()
->numeric()
->minValue(1)
->maxValue(65535)
->default(env('DB_PORT', 3306))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_USERNAME')
->label('Database Username')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your database user.')
->required()
->default(env('DB_USERNAME', 'pelican'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_PASSWORD')
->label('Database Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password of your database user. Can be empty.')
->password()
->revealable()
->default(env('DB_PASSWORD'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
])
->afterValidation(function (Get $get) {
$driver = $get('env.DB_CONNECTION');
if ($driver !== 'sqlite') {
/** @var DatabaseManager $database */
$database = app(DatabaseManager::class);
try {
config()->set('database.connections._panel_install_test', [
'driver' => $driver,
'host' => $get('env.DB_HOST'),
'port' => $get('env.DB_PORT'),
'database' => $get('env.DB_DATABASE'),
'username' => $get('env.DB_USERNAME'),
'password' => $get('env.DB_PASSWORD'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
$database->connection('_panel_install_test')->getPdo();
} catch (PDOException $exception) {
Notification::make()
->title('Database connection failed')
->body($exception->getMessage())
->danger()
->send();
$database->disconnect('_panel_install_test');
throw new Halt('Database connection failed');
}
}
});
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Set;
class EnvironmentStep
{
public const CACHE_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
];
public const SESSION_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
'database' => 'Database',
'cookie' => 'Cookie',
];
public const QUEUE_DRIVERS = [
'database' => 'Database',
'redis' => 'Redis',
'sync' => 'Synchronous',
];
public const DATABASE_DRIVERS = [
'sqlite' => 'SQLite',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
];
public static function make(): Step
{
return Step::make('environment')
->label('Environment')
->columns()
->schema([
TextInput::make('env.APP_NAME')
->label('App Name')
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the Name of your Panel.')
->required()
->default(config('app.name')),
TextInput::make('env.APP_URL')
->label('App URL')
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the URL you access your Panel from.')
->required()
->default(config('app.url'))
->live()
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))),
Toggle::make('env.SESSION_SECURE_COOKIE')
->hidden()
->default(env('SESSION_SECURE_COOKIE')),
ToggleButtons::make('env.CACHE_STORE')
->label('Cache Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
->required()
->inline()
->options(self::CACHE_DRIVERS)
->default(config('cache.default', 'file')),
ToggleButtons::make('env.SESSION_DRIVER')
->label('Session Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
->required()
->inline()
->options(self::SESSION_DRIVERS)
->default(config('session.driver', 'file')),
ToggleButtons::make('env.QUEUE_CONNECTION')
->label('Queue Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->required()
->inline()
->options(self::QUEUE_DRIVERS)
->default(config('queue.default', 'database')),
ToggleButtons::make('env.DB_CONNECTION')
->label('Database Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
->required()
->inline()
->options(self::DATABASE_DRIVERS)
->default(config('database.default', 'sqlite')),
]);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
class RedisStep
{
public static function make(): Step
{
return Step::make('redis')
->label('Redis')
->columns()
->schema([
TextInput::make('env.REDIS_HOST')
->label('Redis Host')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
->required()
->default(config('database.redis.default.host')),
TextInput::make('env.REDIS_PORT')
->label('Redis Port')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your redis server.')
->required()
->default(config('database.redis.default.port')),
TextInput::make('env.REDIS_USERNAME')
->label('Redis Username')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your redis user. Can be empty')
->default(config('database.redis.default.username')),
TextInput::make('env.REDIS_PASSWORD')
->label('Redis Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.')
->password()
->revealable()
->default(config('database.redis.default.password')),
]);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Wizard\Step;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
class RequirementsStep
{
public static function make(): Step
{
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0;
$fields = [
Section::make('PHP Version')
->description('8.2 or newer')
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
->iconColor($correctPhpVersion ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'),
]),
];
$phpExtensions = [
'BCMath' => extension_loaded('bcmath'),
'cURL' => extension_loaded('curl'),
'GD' => extension_loaded('gd'),
'intl' => extension_loaded('intl'),
'mbstring' => extension_loaded('mbstring'),
'MySQL' => extension_loaded('pdo_mysql'),
'SQLite3' => extension_loaded('pdo_sqlite'),
'XML' => extension_loaded('xml'),
'Zip' => extension_loaded('zip'),
];
$allExtensionsInstalled = !in_array(false, $phpExtensions);
$fields[] = Section::make('PHP Extensions')
->description(implode(', ', array_keys($phpExtensions)))
->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x')
->iconColor($allExtensionsInstalled ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('All needed PHP Extensions are installed.')
->visible($allExtensionsInstalled),
Placeholder::make('')
->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false)))
->visible(!$allExtensionsInstalled),
]);
$folderPermissions = [
'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4) >= 755,
'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4) >= 755,
];
$correctFolderPermissions = !in_array(false, $folderPermissions);
$fields[] = Section::make('Folder Permissions')
->description(implode(', ', array_keys($folderPermissions)))
->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x')
->iconColor($correctFolderPermissions ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('All Folders have the correct permissions.')
->visible($correctFolderPermissions),
Placeholder::make('')
->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false)))
->visible(!$correctFolderPermissions),
]);
return Step::make('requirements')
->label('Server Requirements')
->schema($fields)
->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) {
if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) {
Notification::make()
->title('Some requirements are missing!')
->danger()
->send();
throw new Halt();
}
});
}
}

View File

@@ -0,0 +1,573 @@
<?php
namespace App\Filament\Pages;
use App\Models\Backup;
use App\Notifications\MailTested;
use App\Traits\Commands\EnvironmentWriterTrait;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification;
/**
* @property Form $form
*/
class Settings extends Page implements HasForms
{
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;
use InteractsWithHeaderActions;
protected static ?string $navigationIcon = 'tabler-settings';
protected static ?string $navigationGroup = 'Advanced';
protected static string $view = 'filament.pages.settings';
public ?array $data = [];
public function mount(): void
{
$this->form->fill();
}
protected function getFormSchema(): array
{
return [
Tabs::make('Tabs')
->columns()
->persistTabInQueryString()
->tabs([
Tab::make('general')
->label('General')
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('recaptcha')
->label('reCAPTCHA')
->icon('tabler-shield')
->schema($this->recaptchaSettings()),
Tab::make('mail')
->label('Mail')
->icon('tabler-mail')
->schema($this->mailSettings()),
Tab::make('backup')
->label('Backup')
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('misc')
->label('Misc')
->icon('tabler-tool')
->schema($this->miscSettings()),
]),
];
}
private function generalSettings(): array
{
return [
TextInput::make('APP_NAME')
->label('App Name')
->required()
->default(env('APP_NAME', 'Pelican')),
TextInput::make('APP_FAVICON')
->label('App Favicon')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.')
->required()
->default(env('APP_FAVICON', './pelican.ico')),
Toggle::make('APP_DEBUG')
->label('Enable Debug Mode?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
->default(env('APP_DEBUG', config('app.debug'))),
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
->label('Navigation')
->inline()
->options([
false => 'Sidebar',
true => 'Topbar',
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
->label('Unit prefix')
->inline()
->options([
false => 'Decimal Prefix (MB/ GB)',
true => 'Binary Prefix (MiB/ GiB)',
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state))
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
ToggleButtons::make('APP_2FA_REQUIRED')
->label('2FA Requirement')
->inline()
->options([
0 => 'Not required',
1 => 'Required for only Admins',
2 => 'Required for all Users',
])
->formatStateUsing(fn ($state): int => (int) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
TagsInput::make('TRUSTED_PROXIES')
->label('Trusted Proxies')
->separator()
->splitKeys(['Tab', ' '])
->placeholder('New IP or IP Range')
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
->hintActions([
FormAction::make('clear')
->label('Clear')
->color('danger')
->icon('tabler-trash')
->requiresConfirmation()
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
FormAction::make('cloudflare')
->label('Set to Cloudflare IPs')
->icon('tabler-brand-cloudflare')
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'141.101.64.0/18',
'108.162.192.0/18',
'190.93.240.0/20',
'188.114.96.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'162.158.0.0/15',
'104.16.0.0/13',
'104.24.0.0/14',
'172.64.0.0/13',
'131.0.72.0/22',
])),
]),
];
}
private function recaptchaSettings(): array
{
return [
Toggle::make('RECAPTCHA_ENABLED')
->label('Enable reCAPTCHA?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
TextInput::make('RECAPTCHA_DOMAIN')
->label('Domain')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
TextInput::make('RECAPTCHA_WEBSITE_KEY')
->label('Website Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
TextInput::make('RECAPTCHA_SECRET_KEY')
->label('Secret Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
];
}
private function mailSettings(): array
{
return [
ToggleButtons::make('MAIL_MAILER')
->label('Mail Driver')
->columnSpanFull()
->inline()
->options([
'log' => 'Print mails to Log',
'smtp' => 'SMTP Server',
'sendmail' => 'sendmail Binary',
'mailgun' => 'Mailgun',
'mandrill' => 'Mandrill',
'postmark' => 'Postmark',
])
->live()
->default(env('MAIL_MAILER', config('mail.default')))
->hintAction(
FormAction::make('test')
->label('Send Test Mail')
->icon('tabler-send')
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
->action(function () {
try {
MailNotification::route('mail', auth()->user()->email)
->notify(new MailTested(auth()->user()));
Notification::make()
->title('Test Mail sent')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Test Mail failed')
->body($exception->getMessage())
->danger()
->send();
}
})
),
Section::make('"From" Settings')
->description('Set the Address and Name used as "From" in mails.')
->columns()
->schema([
TextInput::make('MAIL_FROM_ADDRESS')
->label('From Address')
->required()
->email()
->default(env('MAIL_FROM_ADDRESS', config('mail.from.address'))),
TextInput::make('MAIL_FROM_NAME')
->label('From Name')
->required()
->default(env('MAIL_FROM_NAME', config('mail.from.name'))),
]),
Section::make('SMTP Configuration')
->columns()
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
->schema([
TextInput::make('MAIL_HOST')
->label('SMTP Host')
->required()
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
TextInput::make('MAIL_PORT')
->label('SMTP Port')
->required()
->numeric()
->minValue(1)
->maxValue(65535)
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
TextInput::make('MAIL_USERNAME')
->label('SMTP Username')
->required()
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
TextInput::make('MAIL_PASSWORD')
->label('SMTP Password')
->password()
->revealable()
->default(env('MAIL_PASSWORD')),
ToggleButtons::make('MAIL_ENCRYPTION')
->label('SMTP encryption')
->required()
->inline()
->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'])
->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))),
]),
Section::make('Mailgun Configuration')
->columns()
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
->schema([
TextInput::make('MAILGUN_DOMAIN')
->label('Mailgun Domain')
->required()
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
TextInput::make('MAILGUN_SECRET')
->label('Mailgun Secret')
->required()
->default(env('MAIL_USERNAME', config('services.mailgun.secret'))),
TextInput::make('MAILGUN_ENDPOINT')
->label('Mailgun Endpoint')
->required()
->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))),
]),
];
}
private function backupSettings(): array
{
return [
ToggleButtons::make('APP_BACKUP_DRIVER')
->label('Backup Driver')
->columnSpanFull()
->inline()
->options([
Backup::ADAPTER_DAEMON => 'Wings',
Backup::ADAPTER_AWS_S3 => 'S3',
])
->live()
->default(env('APP_BACKUP_DRIVER', config('backups.default'))),
Section::make('Throttles')
->description('Configure how many backups can be created in a period. Set period to 0 to disable this throttle.')
->columns()
->schema([
TextInput::make('BACKUP_THROTTLE_LIMIT')
->label('Limit')
->required()
->numeric()
->minValue(1)
->default(config('backups.throttles.limit')),
TextInput::make('BACKUP_THROTTLE_PERIOD')
->label('Period')
->required()
->numeric()
->minValue(0)
->suffix('Seconds')
->default(config('backups.throttles.period')),
]),
Section::make('S3 Configuration')
->columns()
->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3)
->schema([
TextInput::make('AWS_DEFAULT_REGION')
->label('Default Region')
->required()
->default(config('backups.disks.s3.region')),
TextInput::make('AWS_ACCESS_KEY_ID')
->label('Access Key ID')
->required()
->default(config('backups.disks.s3.key')),
TextInput::make('AWS_SECRET_ACCESS_KEY')
->label('Secret Access Key')
->required()
->default(config('backups.disks.s3.secret')),
TextInput::make('AWS_BACKUPS_BUCKET')
->label('Bucket')
->required()
->default(config('backups.disks.s3.bucket')),
TextInput::make('AWS_ENDPOINT')
->label('Endpoint')
->required()
->default(config('backups.disks.s3.endpoint')),
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
->label('Use path style endpoint?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('AWS_USE_PATH_STYLE_ENDPOINT', (bool) $state))
->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))),
]),
];
}
private function miscSettings(): array
{
return [
Section::make('Automatic Allocation Creation')
->description('Toggle if Users can create allocations via the client area.')
->columns()
->collapsible()
->collapsed()
->schema([
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
->label('Allow Users to create allocations?')
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_CLIENT_ALLOCATIONS_ENABLED', (bool) $state))
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
->label('Starting Port')
->required()
->numeric()
->minValue(1024)
->maxValue(65535)
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_START')),
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_END')
->label('Ending Port')
->required()
->numeric()
->minValue(1024)
->maxValue(65535)
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_END')),
]),
Section::make('Mail Notifications')
->description('Toggle which mail notifications should be sent to Users.')
->columns()
->collapsible()
->collapsed()
->schema([
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
->label('Server Installed')
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
->label('Server Reinstalled')
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
]),
Section::make('Connections')
->description('Timeouts used when making requests.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('GUZZLE_TIMEOUT')
->label('Request Timeout')
->required()
->numeric()
->minValue(15)
->maxValue(60)
->suffix('Seconds')
->default(env('GUZZLE_TIMEOUT', config('panel.guzzle.timeout'))),
TextInput::make('GUZZLE_CONNECT_TIMEOUT')
->label('Connect Timeout')
->required()
->numeric()
->minValue(5)
->maxValue(60)
->suffix('Seconds')
->default(env('GUZZLE_CONNECT_TIMEOUT', config('panel.guzzle.connect_timeout'))),
]),
Section::make('Activity Logs')
->description('Configure how often old activity logs should be pruned and whether admin activities should be logged.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('APP_ACTIVITY_PRUNE_DAYS')
->label('Prune age')
->required()
->numeric()
->minValue(1)
->maxValue(365)
->suffix('Days')
->default(env('APP_ACTIVITY_PRUNE_DAYS', config('activity.prune_days'))),
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
->label('Hide admin activities?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state))
->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))),
]),
Section::make('API')
->description('Defines the rate limit for the number of requests per minute that can be executed.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('APP_API_CLIENT_RATELIMIT')
->label('Client API Rate Limit')
->required()
->numeric()
->minValue(1)
->suffix('Requests Per Minute')
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
TextInput::make('APP_API_APPLICATION_RATELIMIT')
->label('Application API Rate Limit')
->required()
->numeric()
->minValue(1)
->suffix('Requests Per Minute')
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
]),
];
}
protected function getFormStatePath(): ?string
{
return 'data';
}
protected function hasUnsavedDataChangesAlert(): bool
{
return true;
}
public function save(): void
{
try {
$data = $this->form->getState();
// Convert bools to a string, so they are correctly written to the .env file
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data);
$this->writeToEnvironment($data);
Artisan::call('config:clear');
Artisan::call('queue:restart');
$this->rememberData();
$this->redirect($this->getUrl());
Notification::make()
->title('Settings saved')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Save failed')
->body($exception->getMessage())
->danger()
->send();
}
}
protected function getHeaderActions(): array
{
return [
Action::make('save')
->action('save')
->keyBindings(['mod+s']),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@@ -4,19 +4,18 @@ namespace App\Filament\Resources;
use App\Filament\Resources\ApiKeyResource\Pages;
use App\Models\ApiKey;
use Filament\Resources\Components\Tab;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class ApiKeyResource extends Resource
{
protected static ?string $model = ApiKey::class;
protected static ?string $label = 'API Key';
protected static ?string $navigationIcon = 'tabler-key';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
return static::getModel()::where('key_type', '2')->count() ?: null;
}
public static function canEdit($record): bool
@@ -24,20 +23,6 @@ class ApiKeyResource extends Resource
return false;
}
public function getTabs(): array
{
return [
'all' => Tab::make('All Keys'),
'application' => Tab::make('Application Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_APPLICATION)),
];
}
public function getDefaultActiveTab(): string|int|null
{
return 'application';
}
public static function getRelations(): array
{
return [

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,40 +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(encrypt(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\Select::make('key_type')
Hidden::make('key_type')
->inlineLabel()
->options(function (ApiKey $apiKey) {
$originalOptions = [
//ApiKey::TYPE_NONE => 'None',
ApiKey::TYPE_ACCOUNT => 'Account',
ApiKey::TYPE_APPLICATION => 'Application',
//ApiKey::TYPE_DAEMON_USER => 'Daemon User',
//ApiKey::TYPE_DAEMON_APPLICATION => 'Daemon Application',
];
->default(ApiKey::TYPE_APPLICATION)
->required(),
return collect($originalOptions)
->filter(fn ($value, $key) => $key <= ApiKey::TYPE_APPLICATION || $apiKey->key_type === $key)
->all();
})
->selectablePlaceholder(false)
->required()
->default(ApiKey::TYPE_APPLICATION),
Forms\Components\Fieldset::make('Permissions')
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',
@@ -81,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

@@ -5,11 +5,10 @@ namespace App\Filament\Resources\ApiKeyResource\Pages;
use App\Filament\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Actions;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Filament\Tables;
class ListApiKeys extends ListRecords
{
@@ -19,42 +18,39 @@ class ListApiKeys extends ListRecords
{
return $table
->searchable(false)
->modifyQueryUsing(fn ($query) => $query->where('key_type', ApiKey::TYPE_APPLICATION))
->columns([
Tables\Columns\TextColumn::make('user.username')
->hidden()
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('key')
TextColumn::make('key')
->copyable()
->icon('tabler-clipboard-text')
->state(fn (ApiKey $key) => $key->identifier . decrypt($key->token)),
->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(),
])
->filters([
//
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(),
//Tables\Actions\EditAction::make()
DeleteAction::make(),
]);
}
@@ -64,22 +60,4 @@ class ListApiKeys extends ListRecords
Actions\CreateAction::make(),
];
}
public function getTabs(): array
{
return [
'all' => Tab::make('All Keys'),
'application' => Tab::make('Application Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_APPLICATION)
),
'account' => Tab::make('Account Keys')
->modifyQueryUsing(fn (Builder $query) => $query->where('key_type', ApiKey::TYPE_ACCOUNT)
),
];
}
public function getDefaultActiveTab(): string|int|null
{
return 'application';
}
}

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.')
@@ -74,25 +80,35 @@ class CreateDatabaseHost extends CreateRecord
]);
}
protected function mutateFormDataBeforeCreate(array $data): array
{
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
}
return $data;
}
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
protected function handleRecordCreation(array $data): Model
{
return resolve(HostCreationService::class)->handle($data);
}
public function exception($e, $stopPropagation): void
{
if ($e instanceof PDOException) {
Notification::make()
->title('Error connecting to database host')
->body($e->getMessage())
->color('danger')
->icon('tabler-database')
->danger()
->send();
$stopPropagation();
}
}
}

View File

@@ -3,11 +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
{
@@ -25,40 +33,39 @@ 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)
->required(),
Forms\Components\Select::make('node_id')
->maxLength(255),
Select::make('node_id')
->searchable()
->preload()
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
@@ -71,20 +78,13 @@ class EditDatabaseHost extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? 'Database Host Has Databases' : 'Delete')
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
$this->getSaveFormAction()->formId('form'),
];
}
protected function mutateFormDataBeforeSave(array $data): array
{
if (isset($data['password'])) {
$data['password'] = encrypt($data['password']);
}
return $data;
}
protected function getFormActions(): array
{
return [];
@@ -93,7 +93,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,39 +4,40 @@ 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
{
protected static string $relationship = 'databases';
protected $listeners = ['refresh' => 'refreshForm'];
public function form(Form $form): Form
{
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')
->requiresConfirmation()
->action(fn (DatabasePasswordService $service, Database $database) => $service->handle($database))
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
)
->formatStateUsing(fn (Database $database) => decrypt($database->password)),
Forms\Components\TextInput::make('remote')->label('Connections From'),
Forms\Components\TextInput::make('max_connections'),
Forms\Components\TextInput::make('JDBC')
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')->label('Connections From'),
TextInput::make('max_connections'),
TextInput::make('JDBC')
->label('JDBC Connection String')
->columnSpanFull()
->formatStateUsing(fn (Forms\Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode(decrypt($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
@@ -44,20 +45,27 @@ 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'),
]);
}
protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void
{
$newPassword = $service->handle($database);
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database');
$set('password', $newPassword);
$set('JDBC', $jdbcString);
}
}

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,27 @@ 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')
->disabled()
->helperText('Not implemented.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\KeyValue::make('docker_images')
TextInput::make('update_url')
->hintIcon('tabler-question-mark')
->hintIconTooltip('URLs must point directly to the raw .json file.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(),
KeyValue::make('docker_images')
->live()
->columnSpanFull()
->required()
@@ -77,37 +90,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 +150,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,66 @@ 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)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1])
->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' => 2, 'lg' => 2])
->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')
->disabled()
->helperText('Not implemented.')
TextInput::make('update_url')
->label('Update URL')
->url()
->hintIcon('tabler-question-mark')
->hintIconTooltip('URLs must point directly to the raw .json file.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Forms\Components\KeyValue::make('docker_images')
KeyValue::make('docker_images')
->live()
->columnSpanFull()
->required()
@@ -85,32 +106,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 +160,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 +219,98 @@ 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')
->default(fn (Egg $egg): ?string => $egg->update_url)
->hint('Link to the egg file (eg. minecraft.json)')
->url(),
]),
])
->contained(false),
])
->action(function (array $data, Egg $egg): void {
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
if (!empty($data['egg'])) {
try {
$eggImportService->fromFile($data['egg'], $egg);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->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 +319,7 @@ class EditEgg extends EditRecord
public function getRelationManagers(): array
{
return [
EggResource\RelationManagers\ServersRelationManager::class,
ServersRelationManager::class,
];
}
}

View File

@@ -4,12 +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;
@@ -21,54 +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 => $record->description)
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
->wrap()
->searchable(),
Tables\Columns\TextColumn::make('author')
->hidden()
->searchable(),
Tables\Columns\TextColumn::make('servers_count')
->searchable()
->sortable(),
TextColumn::make('servers_count')
->counts('servers')
->icon('tabler-server')
->label('Servers'),
Tables\Columns\TextColumn::make('script_container')
->searchable()
->hidden(),
Tables\Columns\TextColumn::make('copyFrom.name')
->hidden()
->sortable(),
Tables\Columns\TextColumn::make('script_entry')
->hidden()
->searchable(),
])
->filters([
//
])
->actions([
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])),
])
->headerActions([
//
->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(),
]),
]);
}
@@ -80,21 +71,57 @@ class ListEggs extends ListRecords
Actions\Action::make('import')
->label('Import')
->form([
Forms\Components\FileUpload::make('egg')
->acceptedFileTypes(['application/json'])
->storeFiles(false)
->multiple(),
Tabs::make('Tabs')
->tabs([
Tab::make('From File')
->icon('tabler-file-upload')
->schema([
FileUpload::make('egg')
->label('Egg')
->hint('This should be the json file ( egg-minecraft.json )')
->acceptedFileTypes(['application/json'])
->storeFiles(false)
->multiple(),
]),
Tab::make('From URL')
->icon('tabler-world-upload')
->schema([
TextInput::make('url')
->label('URL')
->hint('This URL should point to a single json file')
->url(),
]),
])
->contained(false),
])
->action(function (array $data): void {
/** @var TemporaryUploadedFile $eggFile */
$eggFile = $data['egg'];
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
foreach ($eggFile as $file) {
if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg'];
foreach ($eggFile as $file) {
try {
$eggImportService->fromFile($file);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->danger()
->send();
report($exception);
return;
}
}
}
if (!empty($data['url'])) {
try {
$eggImportService->handle($file);
$eggImportService->fromUrl($data['url']);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')

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,11 +340,64 @@ class CreateNode extends CreateRecord
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%'),
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0)
->required(),
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%')
->required(),
]),
]),
]),
]);
])->columnSpanFull()
->nextAction(fn (Action $action) => $action->label('Next Step'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
type="submit"
size="sm"
>
Create Node
</x-filament::button>
BLADE))),
]);
}
protected function getRedirectUrlParameters(): array
@@ -322,4 +406,9 @@ class CreateNode extends CreateRecord
'tab' => '-configuration-tab',
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@@ -3,12 +3,23 @@
namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart;
use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart;
use App\Models\Node;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\View;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
@@ -30,10 +41,36 @@ class EditNode extends EditRecord
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tabs\Tab::make('Basic Settings')
Tab::make('')
->label('Overview')
->icon('tabler-chart-area-line-filled')
->columns(6)
->schema([
Fieldset::make()
->label('Node Information')
->columns(4)
->schema([
Placeholder::make('')
->label('Wings Version')
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
Placeholder::make('')
->label('CPU Threads')
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
Placeholder::make('')
->label('Architecture')
->content(fn (Node $node) => $node->systemInformation()['architecture'] ?? 'Unknown'),
Placeholder::make('')
->label('Kernel')
->content(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? 'Unknown'),
]),
View::make('filament.components.node-cpu-chart')->columnSpan(3),
View::make('filament.components.node-memory-chart')->columnSpan(3),
// TODO: Make purdy View::make('filament.components.node-storage-chart')->columnSpan(3),
]),
Tab::make('Basic Settings')
->icon('tabler-server')
->schema([
Forms\Components\TextInput::make('fqdn')
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
@@ -66,7 +103,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);
@@ -92,19 +129,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',
@@ -121,7 +158,7 @@ class EditNode extends EditRecord
'lg' => 1,
]),
Forms\Components\TextInput::make('daemon_listen')
TextInput::make('daemon_listen')
->columnSpan([
'default' => 1,
'sm' => 1,
@@ -130,13 +167,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,
@@ -149,7 +186,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,
@@ -157,9 +194,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.');
}
@@ -184,27 +220,50 @@ 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())
->columnSpan(2)
->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')
->columnSpan(1),
Forms\Components\ToggleButtons::make('public')
->hintIconTooltip('Not Implemented'),
TextInput::make('upload_size')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->label('Upload Limit')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->numeric()->required()
->minValue(1)
->maxValue(1024)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Port')
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Alias')
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
ToggleButtons::make('public')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Automatic Allocation')->inline()
->columnSpan(1)
->options([
true => 'Yes',
false => 'No',
@@ -213,37 +272,28 @@ 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()
->columnSpan(1)
->hinticon('tabler-question-mark')
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
->options([
true => 'Enable',
false => 'Disable',
true => 'Enable',
])
->colors([
true => 'danger',
false => 'success',
true => 'danger',
]),
Forms\Components\TextInput::make('upload_size')
->label('Upload Limit')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
->columnStart(4)->columnSpan(1)
->numeric()->required()
->minValue(1)
->maxValue(1024)
->suffix('MiB'),
Forms\Components\Grid::make()
->columns(6)
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',
@@ -253,39 +303,79 @@ class EditNode extends EditRecord
true => 'primary',
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
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(2)
->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(2)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
->schema([
ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->numeric()
->minValue(0),
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->required()
->numeric()
->minValue(-1)
->maxValue(100)
->suffix('%'),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
->label('Disk')->inlineLabel()->inline()
ToggleButtons::make('unlimited_cpu')
->label('CPU')->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('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
@@ -295,18 +385,18 @@ class EditNode extends EditRecord
false => 'warning',
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->label('Disk Limit')->inlineLabel()
->suffix('MiB')
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
Forms\Components\TextInput::make('disk_overallocate')
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('Overallocate')->inlineLabel()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The % allowable to go over the set limit.')
@@ -318,20 +408,33 @@ 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)
->hintAction(CopyAction::make())
->columnSpanFull(),
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('resetKey')
->label('Reset Daemon Token')
->color('danger')
->requiresConfirmation()
->modalHeading('Reset Daemon Token?')
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
->action(function (NodeUpdateService $nodeUpdateService, Node $node) {
$nodeUpdateService->handle($node, [], true);
Notification::make()->success()->title('Daemon Key Reset')->send();
$this->fillForm();
}),
]),
]),
]),
]);
@@ -360,11 +463,17 @@ class EditNode extends EditRecord
];
}
protected function getFooterWidgets(): array
protected function afterSave(): void
{
return [
NodeStorageChart::class,
NodeMemoryChart::class,
];
$this->fillForm();
}
protected function getColumnSpan()
{
return null;
}
protected function getColumnStart()
{
return null;
}
}

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,64 +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\IconColumn::make('scheme')
TextColumn::make('cpu')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(' %')
->sortable(),
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,20 +49,21 @@ class AllocationsRelationManager extends RelationManager
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->searchable()
->columns([
Tables\Columns\TextColumn::make('server.name')
TextColumn::make('id'),
TextColumn::make('port')
->searchable()
->label('Port'),
TextColumn::make('server.name')
->label('Server')
->icon('tabler-brand-docker')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
Tables\Columns\TextInputColumn::make('ip_alias')
TextInputColumn::make('ip_alias')
->searchable()
->label('Alias'),
Tables\Columns\TextInputColumn::make('ip')
TextInputColumn::make('ip')
->searchable()
->label('IP'),
Tables\Columns\TextColumn::make('port')
->searchable()
->label('Port'),
])
->filters([
//
@@ -64,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.
@@ -87,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) {
@@ -112,7 +122,7 @@ class AllocationsRelationManager extends RelationManager
$start = max((int) $start, 0);
$end = min((int) $end, 2 ** 16 - 1);
for ($i = $start; $i <= $end; $i++) {
foreach (range($start, $end) as $i) {
$ports->push($i);
}
}
@@ -141,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')

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Filament\Resources\NodeResource\Widgets;
use App\Models\Node;
use Carbon\Carbon;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Database\Eloquent\Model;
class NodeCpuChart extends ChartWidget
{
protected static ?string $pollingInterval = '5s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;
protected function getData(): array
{
/** @var Node $node */
$node = $this->record;
$threads = $node->systemInformation()['cpu_count'] ?? 0;
$cpu = collect(cache()->get("nodes.$node->id.cpu_percent"))
->slice(-10)
->map(fn ($value, $key) => [
'cpu' => number_format($value * $threads, 2),
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
])
->all();
return [
'datasets' => [
[
'data' => array_column($cpu, 'cpu'),
'backgroundColor' => [
'rgba(96, 165, 250, 0.3)',
],
'tension' => '0.3',
'fill' => true,
],
],
'labels' => array_column($cpu, 'timestamp'),
];
}
protected function getType(): string
{
return 'line';
}
protected function getOptions(): RawJs
{
return RawJs::make(<<<'JS'
{
scales: {
y: {
min: 0,
},
},
plugins: {
legend: {
display: false,
}
}
}
JS);
}
public function getHeading(): string
{
/** @var Node $node */
$node = $this->record;
$threads = $node->systemInformation()['cpu_count'] ?? 0;
$cpu = number_format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, 2);
$max = number_format($threads * 100) . '%';
return 'CPU - ' . $cpu . '% Of ' . $max;
}
}

View File

@@ -3,66 +3,83 @@
namespace App\Filament\Resources\NodeResource\Widgets;
use App\Models\Node;
use Carbon\Carbon;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Database\Eloquent\Model;
class NodeMemoryChart extends ChartWidget
{
protected static ?string $heading = 'Memory';
protected static ?string $pollingInterval = '60s';
protected static ?string $pollingInterval = '5s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;
protected static ?array $options = [
'scales' => [
'x' => [
'grid' => [
'display' => false,
],
'ticks' => [
'display' => false,
],
],
'y' => [
'grid' => [
'display' => false,
],
'ticks' => [
'display' => false,
],
],
],
];
protected function getData(): array
{
/** @var Node $node */
$node = $this->record;
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
$unused = $total - $used;
$memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10)
->map(fn ($value, $key) => [
'memory' => config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000,
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
])
->all();
return [
'datasets' => [
[
'label' => 'Data Cool',
'data' => [$used, $unused],
'data' => array_column($memUsed, 'memory'),
'backgroundColor' => [
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 205, 86)',
'rgba(96, 165, 250, 0.3)',
],
'tension' => '0.3',
'fill' => true,
],
// 'backgroundColor' => [],
],
'labels' => ['Used', 'Unused'],
'labels' => array_column($memUsed, 'timestamp'),
];
}
protected function getType(): string
{
return 'pie';
return 'line';
}
protected function getOptions(): RawJs
{
return RawJs::make(<<<'JS'
{
scales: {
y: {
min: 0,
},
},
plugins: {
legend: {
display: false,
}
}
}
JS);
}
public function getHeading(): string
{
/** @var Node $node */
$node = $this->record;
$latestMemoryUsed = collect(cache()->get("nodes.$node->id.memory_used"))->last();
$totalMemory = collect(cache()->get("nodes.$node->id.memory_total"))->last();
$used = config('panel.use_binary_prefix')
? number_format($latestMemoryUsed / 1024 / 1024 / 1024, 2) .' GiB'
: number_format($latestMemoryUsed / 1000 / 1000 / 1000, 2) . ' GB';
$total = config('panel.use_binary_prefix')
? number_format($totalMemory / 1024 / 1024 / 1024, 2) .' GiB'
: number_format($totalMemory / 1000 / 1000 / 1000, 2) . ' GB';
return 'Memory - ' . $used . ' Of ' . $total;
}
}

View File

@@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Model;
class NodeStorageChart extends ChartWidget
{
protected static ?string $heading = 'Storage';
protected static ?string $pollingInterval = '60s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;
@@ -47,7 +47,6 @@ class NodeStorageChart extends ChartWidget
return [
'datasets' => [
[
'label' => 'Data Cool',
'data' => [$used, $unused],
'backgroundColor' => [
'rgb(255, 99, 132)',
@@ -55,7 +54,6 @@ class NodeStorageChart extends ChartWidget
'rgb(255, 205, 86)',
],
],
// 'backgroundColor' => [],
],
'labels' => ['Used', 'Unused'],
];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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,20 @@
namespace App\Filament\Resources\ServerResource\RelationManagers;
use App\Models\Allocation;
use Filament\Forms;
use App\Models\Server;
use App\Services\Allocations\AssignmentService;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Set;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;
/**
* @method Server getOwnerRecord()
*/
class AllocationsRelationManager extends RelationManager
{
protected static string $relationship = 'allocations';
@@ -17,7 +25,7 @@ class AllocationsRelationManager extends RelationManager
{
return $form
->schema([
Forms\Components\TextInput::make('ip')
TextInput::make('ip')
->required()
->maxLength(255),
]);
@@ -27,21 +35,23 @@ class AllocationsRelationManager extends RelationManager
{
return $table
->recordTitleAttribute('ip')
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
// ->actions
// ->groups
->inverseRelationship('server')
->columns([
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
Tables\Columns\TextColumn::make('ip')->label('IP'),
Tables\Columns\TextColumn::make('port')->label('Port'),
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
Tables\Columns\IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
false => 'tabler-star',
true => 'tabler-star-filled',
default => 'tabler-star',
})
->color(fn ($state) => match ($state) {
false => 'gray',
true => 'warning',
default => 'gray',
})
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
@@ -56,13 +66,94 @@ class AllocationsRelationManager extends RelationManager
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
])
->headerActions([
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
//TODO Tables\Actions\AssociateAction::make()->label('Add Allocation'),
Tables\Actions\CreateAction::make()->label('Create Allocation')
->createAnother(false)
->form(fn () => [
TextInput::make('allocation_ip')
->datalist($this->getOwnerRecord()->node->ipAddresses())
->label('IP Address')
->inlineLabel()
->ipv4()
->helperText("Usually your machine's public IP unless you are port forwarding.")
->required(),
TextInput::make('allocation_alias')
->label('Alias')
->inlineLabel()
->default(null)
->helperText('Optional display name to help you remember what these are.')
->required(false),
TagsInput::make('allocation_ports')
->placeholder('Examples: 27015, 27017-27019')
->helperText(new HtmlString('
These are the ports that users can connect to this Server through.
<br />
You would have to port forward these on your home network.
'))
->label('Ports')
->inlineLabel()
->live()
->afterStateUpdated(function ($state, Set $set) {
$ports = collect();
$update = false;
foreach ($state as $portEntry) {
if (!str_contains($portEntry, '-')) {
if (is_numeric($portEntry)) {
$ports->push((int) $portEntry);
continue;
}
// Do not add non numerical ports
$update = true;
continue;
}
$update = true;
[$start, $end] = explode('-', $portEntry);
if (!is_numeric($start) || !is_numeric($end)) {
continue;
}
$start = max((int) $start, 0);
$end = min((int) $end, 2 ** 16 - 1);
foreach (range($start, $end) as $i) {
$ports->push($i);
}
}
$uniquePorts = $ports->unique()->values();
if ($ports->count() > $uniquePorts->count()) {
$update = true;
$ports = $uniquePorts;
}
$sortedPorts = $ports->sort()->values();
if ($sortedPorts->all() !== $ports->all()) {
$update = true;
$ports = $sortedPorts;
}
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
if ($update) {
$set('allocation_ports', $ports->all());
}
})
->splitKeys(['Tab', ' ', ','])
->required(),
])
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
Tables\Actions\AssociateAction::make()
->multiple()
->associateAnother(false)
->preloadRecordSelect()
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
->label('Add Allocation'),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DissociateBulkAction::make(),
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

@@ -2,15 +2,18 @@
namespace App\Filament\Resources\UserResource\Pages;
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
use App\Facades\Activity;
use App\Models\ActivityLog;
use App\Models\ApiKey;
use App\Models\User;
use App\Services\Users\ToggleTwoFactorService;
use App\Services\Users\TwoFactorSetupService;
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Common\Version;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use DateTimeZone;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
@@ -20,13 +23,18 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rules\Password;
/**
* @method User getUser()
*/
class EditProfile extends \Filament\Pages\Auth\EditProfile
{
protected function getForms(): array
@@ -45,7 +53,8 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->label(trans('strings.username'))
->disabled()
->readOnly()
->maxLength(191)
->dehydrated(false)
->maxLength(255)
->unique(ignoreRecord: true)
->autofocus(),
@@ -54,7 +63,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->label(trans('strings.email'))
->email()
->required()
->maxLength(191)
->maxLength(255)
->unique(ignoreRecord: true),
TextInput::make('password')
@@ -78,6 +87,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()
@@ -99,15 +114,32 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
if ($this->getUser()->use_totp) {
return [
Placeholder::make('2FA already enabled!'),
Placeholder::make('2fa-already-enabled')
->label('Two Factor Authentication is currently enabled!'),
Textarea::make('backup-tokens')
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
->rows(10)
->readOnly()
->dehydrated(false)
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
->helperText('These will not be shown again!')
->label('Backup Tokens:'),
TextInput::make('2fa-disable-code')
->label('Disable 2FA')
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
];
}
/** @var TwoFactorSetupService */
$setupService = app(TwoFactorSetupService::class);
['image_url_data' => $url] = $setupService->handle($this->getUser());
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
"users.{$this->getUser()->id}.2fa.state",
now()->addMinutes(5), fn () => $setupService->handle($this->getUser())
);
$options = new QROptions([
'svgLogo' => public_path('pelican.svg'),
'svgLogoScale' => 0.05,
'addLogoSpace' => true,
'logoSpaceWidth' => 13,
'logoSpaceHeight' => 13,
@@ -115,22 +147,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"/>
@@ -147,9 +181,19 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
Placeholder::make('qr')
->label('Scan QR Code')
->content(fn () => new HtmlString("
<div style='width: 300px'>$image</div>
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
"))
->default('asdfasdf'),
->helperText('Setup Key: '. $secret),
TextInput::make('2facode')
->label('Code')
->requiredWith('2fapassword')
->helperText('Scan the QR code above using your two-step authentication app, then enter the code generated.'),
TextInput::make('2fapassword')
->label('Current Password')
->requiredWith('2facode')
->currentPassword()
->password()
->helperText('Enter your current password to verify.'),
];
}),
@@ -158,8 +202,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'),
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')
@@ -167,9 +215,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 $user) {
$token = $user->createToken(
$get('description'),
$get('allowed_ips'),
);
@@ -182,8 +231,9 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
$action->success();
}),
]),
Section::make('API Keys')->columnSpan(2)->schema([
Section::make('Keys')->columnSpan(2)->schema([
Repeater::make('keys')
->label('')
->relationship('apiKeys')
->addable(false)
->itemLabel(fn ($state) => $state['identifier'])
@@ -235,4 +285,43 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
),
];
}
protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloquent\Model
{
if ($token = $data['2facode'] ?? null) {
/** @var ToggleTwoFactorService $service */
$service = resolve(ToggleTwoFactorService::class);
$tokens = $service->handle($record, $token, true);
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
}
if ($token = $data['2fa-disable-code'] ?? null) {
/** @var ToggleTwoFactorService $service */
$service = resolve(ToggleTwoFactorService::class);
$service->handle($record, $token, false);
cache()->forget("users.$record->id.2fa.state");
}
return parent::handleRecordUpdate($record, $data);
}
public function exception($e, $stopPropagation): void
{
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
Notification::make()
->title('Invalid 2FA Code')
->body($e->getMessage())
->color('danger')
->icon('tabler-2fa')
->danger()
->send();
$stopPropagation();
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use App\Services\Exceptions\FilamentExceptionHandler;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Models\User;
@@ -19,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))
@@ -66,7 +67,9 @@ class EditUser extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
Actions\DeleteAction::make()
->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete'))
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
$this->getSaveFormAction()->formId('form'),
];
}
@@ -75,4 +78,9 @@ class EditUser extends EditRecord
{
return [];
}
public function exception($exception, $stopPropagation): void
{
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
}
}

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
) {
}
@@ -46,7 +44,7 @@ class EggShareController extends Controller
*/
public function import(EggImportFormRequest $request): RedirectResponse
{
$egg = $this->importerService->handle($request->file('import_file'));
$egg = $this->importerService->fromFile($request->file('import_file'));
$this->alert->success(trans('admin/eggs.notices.imported'))->flash();
return redirect()->route('admin.eggs.view', ['egg' => $egg->id]);
@@ -61,7 +59,7 @@ class EggShareController extends Controller
*/
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
{
$this->updateImporterService->handle($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

@@ -56,7 +56,7 @@ class NodeAutoDeployController extends Controller
return new JsonResponse([
'node' => $node->id,
'token' => $key->identifier . decrypt($key->token),
'token' => $key->identifier . $key->token,
]);
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Node;
use Spatie\QueryBuilder\QueryBuilder;
use App\Http\Controllers\Controller;
@@ -13,7 +12,7 @@ class NodeController extends Controller
/**
* Returns a listing of nodes on the system.
*/
public function index(Request $request): View
public function index(): View
{
$nodes = QueryBuilder::for(
Node::query()->withCount('servers')

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Node;
use Illuminate\Support\Collection;
use App\Models\Allocation;
@@ -29,16 +28,10 @@ class NodeViewController extends Controller
/**
* Returns index view for a specific node on the system.
*/
public function index(Request $request, Node $node): View
public function index(Node $node): View
{
$node->loadCount('servers');
$stats = Node::query()
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
->join('servers', 'servers.node_id', '=', 'nodes.id')
->where('node_id', '=', $node->id)
->first();
return view('admin.nodes.view.index', [
'node' => $node,
'version' => $this->versionService,
@@ -48,7 +41,7 @@ class NodeViewController extends Controller
/**
* Returns the settings page for a specific node.
*/
public function settings(Request $request, Node $node): View
public function settings(Node $node): View
{
return view('admin.nodes.view.settings', [
'node' => $node,
@@ -58,7 +51,7 @@ class NodeViewController extends Controller
/**
* Return the node configuration page for a specific node.
*/
public function configuration(Request $request, Node $node): View
public function configuration(Node $node): View
{
return view('admin.nodes.view.configuration', compact('node'));
}
@@ -66,7 +59,7 @@ class NodeViewController extends Controller
/**
* Return the node allocation management page.
*/
public function allocations(Request $request, Node $node): View
public function allocations(Node $node): View
{
$node->setRelation(
'allocations',
@@ -92,7 +85,7 @@ class NodeViewController extends Controller
/**
* Return a listing of servers that exist for this specific node.
*/
public function servers(Request $request, Node $node): View
public function servers(Node $node): View
{
$this->plainInject([
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))

View File

@@ -53,7 +53,6 @@ class CreateServerController extends Controller
* @throws \Illuminate\Validation\ValidationException
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
* @throws \Throwable
*/
public function store(ServerFormRequest $request): RedirectResponse

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Admin\Servers;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Server;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
@@ -16,7 +15,7 @@ class ServerController extends Controller
* Returns all the servers that exist on the system using a paginated result set. If
* a query is passed along in the request it is also passed to the repository function.
*/
public function index(Request $request): View
public function index(): View
{
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
->allowedFilters([

View File

@@ -3,13 +3,13 @@
namespace App\Http\Controllers\Admin;
use App\Enums\ServerState;
use Filament\Notifications\Notification;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Http\Response;
use App\Models\Mount;
use App\Models\Server;
use App\Models\Database;
use App\Models\MountServer;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use App\Exceptions\DisplayException;
@@ -70,7 +70,7 @@ class ServersController extends Controller
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function toggleInstall(Server $server): RedirectResponse
public function toggleInstall(Server $server)
{
if ($server->status === ServerState::InstallFailed) {
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
@@ -79,9 +79,13 @@ class ServersController extends Controller
$server->status = $server->isInstalled() ? ServerState::Installing : null;
$server->save();
$this->alert->success(trans('admin/server.alerts.install_toggled'))->flash();
Notification::make()
->title('Success!')
->body(trans('admin/server.alerts.install_toggled'))
->success()
->send();
return redirect()->route('admin.servers.view.manage', $server->id);
return null;
}
/**
@@ -90,12 +94,15 @@ class ServersController extends Controller
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function reinstallServer(Server $server): RedirectResponse
public function reinstallServer(Server $server)
{
$this->reinstallService->handle($server);
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
Notification::make()
->title('Success!')
->body(trans('admin/server.alerts.server_reinstalled'))
->success()
->send();
}
/**
@@ -228,12 +235,7 @@ class ServersController extends Controller
*/
public function addMount(Request $request, Server $server): RedirectResponse
{
$mountServer = (new MountServer())->forceFill([
'mount_id' => $request->input('mount_id'),
'server_id' => $server->id,
]);
$mountServer->saveOrFail();
$server->mounts()->attach($request->input('mount_id'));
$this->alert->success('Mount was added successfully.')->flash();
@@ -245,7 +247,7 @@ class ServersController extends Controller
*/
public function deleteMount(Server $server, Mount $mount): RedirectResponse
{
MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete();
$server->mounts()->detach($mount);
$this->alert->success('Mount was removed successfully.')->flash();

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Models\Setting;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest;
class AdvancedController extends Controller
{
/**
* AdvancedController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private Kernel $kernel,
) {
}
/**
* Render advanced Panel settings UI.
*/
public function index(): View
{
$showRecaptchaWarning = false;
if (
config('recaptcha._shipped_secret_key') === config('recaptcha.secret_key')
|| config('recaptcha._shipped_website_key') === config('recaptcha.website_key')
) {
$showRecaptchaWarning = true;
}
return view('admin.settings.advanced', [
'showRecaptchaWarning' => $showRecaptchaWarning,
]);
}
/**
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(AdvancedSettingsFormRequest $request): RedirectResponse
{
foreach ($request->normalize() as $key => $value) {
Setting::set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings.advanced');
}
}

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Models\Setting;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use App\Http\Controllers\Controller;
use App\Traits\Helpers\AvailableLanguages;
use App\Services\Helpers\SoftwareVersionService;
use App\Http\Requests\Admin\Settings\BaseSettingsFormRequest;
class IndexController extends Controller
{
use AvailableLanguages;
/**
* IndexController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private Kernel $kernel,
private SoftwareVersionService $versionService,
) {
}
/**
* Render the UI for basic Panel settings.
*/
public function index(): View
{
return view('admin.settings.index', [
'version' => $this->versionService,
'languages' => $this->getAvailableLanguages(),
]);
}
/**
* Handle settings update.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(BaseSettingsFormRequest $request): RedirectResponse
{
foreach ($request->normalize() as $key => $value) {
Setting::set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings');
}
}

View File

@@ -1,82 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Models\Setting;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Contracts\Console\Kernel;
use App\Notifications\MailTested;
use Illuminate\Support\Facades\Notification;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use App\Providers\SettingsServiceProvider;
use App\Http\Requests\Admin\Settings\MailSettingsFormRequest;
class MailController extends Controller
{
/**
* MailController constructor.
*/
public function __construct(
private Kernel $kernel,
) {
}
/**
* Render UI for editing mail settings. This UI should only display if
* the server is configured to send mail using SMTP.
*/
public function index(): View
{
return view('admin.settings.mail', [
'disabled' => config('mail.default') !== 'smtp',
]);
}
/**
* Handle request to update SMTP mail settings.
*
* @throws DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(MailSettingsFormRequest $request): Response
{
if (config('mail.default') !== 'smtp') {
throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.');
}
$values = $request->normalize();
if (array_get($values, 'mail:mailers:smtp:password') === '!e') {
$values['mail:mailers:smtp:password'] = '';
}
foreach ($values as $key => $value) {
if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) {
$value = encrypt($value);
}
Setting::set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
return response('', 204);
}
/**
* Submit a request to send a test mail message.
*/
public function test(Request $request): Response
{
try {
Notification::route('mail', $request->user()->email)
->notify(new MailTested($request->user()));
} catch (\Exception $exception) {
return response($exception->getMessage(), 500);
}
return response('', 204);
}
}

View File

@@ -37,7 +37,7 @@ class UserController extends Controller
/**
* Display user index page.
*/
public function index(Request $request): View
public function index(): View
{
$users = QueryBuilder::for(
User::query()->select('users.*')

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers\Api\Application\Mounts;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Contracts\Translation\Translator;
use Spatie\QueryBuilder\QueryBuilder;
use App\Models\Mount;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Transformers\Api\Application\MountTransformer;
use App\Http\Requests\Api\Application\Mounts\GetMountRequest;
use App\Http\Requests\Api\Application\Mounts\StoreMountRequest;
use App\Http\Requests\Api\Application\Mounts\DeleteMountRequest;
use App\Http\Requests\Api\Application\Mounts\UpdateMountRequest;
use App\Exceptions\Service\HasActiveServersException;
class MountController extends ApplicationApiController
{
/**
* MountController constructor.
*/
public function __construct(
protected Translator $translator
) {
parent::__construct();
}
/**
* Return all the mounts currently available on the Panel.
*/
public function index(GetMountRequest $request): array
{
$mounts = QueryBuilder::for(Mount::query())
->allowedFilters(['uuid', 'name'])
->allowedSorts(['id', 'uuid'])
->paginate($request->query('per_page') ?? 50);
return $this->fractal->collection($mounts)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Return data for a single instance of a mount.
*/
public function view(GetMountRequest $request, Mount $mount): array
{
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Create a new mount on the Panel. Returns the created mount and an HTTP/201
* status response on success.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function store(StoreMountRequest $request): JsonResponse
{
$model = (new Mount())->fill($request->validated());
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
$model->saveOrFail();
$mount = $model->fresh();
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->addMeta([
'resource' => route('api.application.mounts.view', [
'mount' => $mount->id,
]),
])
->respond(201);
}
/**
* Update an existing mount on the Panel.
*
* @throws \Throwable
*/
public function update(UpdateMountRequest $request, Mount $mount): array
{
$mount->forceFill($request->validated())->save();
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Deletes a given mount from the Panel as long as there are no servers
* currently attached to it.
*
* @throws \App\Exceptions\Service\HasActiveServersException
*/
public function delete(DeleteMountRequest $request, Mount $mount): JsonResponse
{
if ($mount->servers()->count() > 0) {
throw new HasActiveServersException($this->translator->get('exceptions.mount.servers_attached'));
}
$mount->delete();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Adds eggs to the mount's many-to-many relation.
*/
public function addEggs(Request $request, Mount $mount): array
{
$validatedData = $request->validate([
'eggs' => 'required|exists:eggs,id',
]);
$eggs = $validatedData['eggs'] ?? [];
if (count($eggs) > 0) {
$mount->eggs()->attach($eggs);
}
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Adds nodes to the mount's many-to-many relation.
*/
public function addNodes(Request $request, Mount $mount): array
{
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
$nodes = $data['nodes'] ?? [];
if (count($nodes) > 0) {
$mount->nodes()->attach($nodes);
}
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Deletes an egg from the mount's many-to-many relation.
*/
public function deleteEgg(Mount $mount, int $egg_id): JsonResponse
{
$mount->eggs()->detach($egg_id);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Deletes a node from the mount's many-to-many relation.
*/
public function deleteNode(Mount $mount, int $node_id): JsonResponse
{
$mount->nodes()->detach($node_id);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@@ -36,7 +36,7 @@ class NodeController extends ApplicationApiController
{
$nodes = QueryBuilder::for(Node::query())
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
->allowedSorts(['id', 'uuid', 'memory', 'disk'])
->allowedSorts(['id', 'uuid', 'memory', 'disk', 'cpu'])
->paginate($request->query('per_page') ?? 50);
return $this->fractal->collection($nodes)

View File

@@ -9,9 +9,6 @@ use App\Http\Requests\Api\Application\Nodes\GetDeployableNodesRequest;
class NodeDeploymentController extends ApplicationApiController
{
/**
* NodeDeploymentController constructor.
*/
public function __construct(private FindViableNodesService $viableNodesService)
{
parent::__construct();
@@ -21,16 +18,17 @@ class NodeDeploymentController extends ApplicationApiController
* Finds any nodes that are available using the given deployment criteria. This works
* similarly to the server creation process, but allows you to pass the deployment object
* to this endpoint and get back a list of all Nodes satisfying the requirements.
*
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
*/
public function __invoke(GetDeployableNodesRequest $request): array
{
$data = $request->validated();
$nodes = $this->viableNodesService
->setMemory($data['memory'])
->setDisk($data['disk'])
->handle((int) $request->query('per_page'), (int) $request->query('page'));
$nodes = $this->viableNodesService->handle(
$data['memory'] ?? 0,
$data['disk'] ?? 0,
$data['cpu'] ?? 0,
$data['tags'] ?? $data['location_ids'] ?? [],
);
return $this->fractal->collection($nodes)
->transformWith($this->getTransformer(NodeTransformer::class))

View File

@@ -50,7 +50,6 @@ class ServerController extends ApplicationApiController
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
*/
public function store(StoreServerRequest $request): JsonResponse
{

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

@@ -65,9 +65,7 @@ class LoginCheckpointController extends AbstractLoginController
return $this->sendLoginResponse($user, $request);
}
} else {
$decrypted = decrypt($user->totp_secret);
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
if ($this->google2FA->verifyKey($user->totp_secret, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
Event::dispatch(new ProvidedAuthenticationToken($user));
return $this->sendLoginResponse($user, $request);

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

@@ -41,7 +41,7 @@ class DaemonAuthenticate
/** @var Node $node */
$node = Node::query()->where('daemon_token_id', $parts[0])->firstOrFail();
if (hash_equals((string) decrypt($node->daemon_token), $parts[1])) {
if (hash_equals((string) $node->daemon_token, $parts[1])) {
$request->attributes->set('node', $node);
return $next($request);

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',

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