Compare commits

...

164 Commits

Author SHA1 Message Date
Lance Pioch
4eeefab6c6 Delete allocation exceptions 2024-10-20 15:50:40 -04:00
Lance Pioch
a2108c3d91 Various improvements 2024-10-20 15:23:46 -04:00
Lance Pioch
4f5e9a6c30 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
#	app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/UserResource/Pages/EditProfile.php
#	app/Models/Node.php
#	app/Models/Objects/DeploymentObject.php
#	app/Services/Allocations/AssignmentService.php
#	app/Services/Servers/ServerCreationService.php
#	app/Services/Servers/TransferServerService.php
#	pint.json
2024-10-20 15:14:08 -04:00
Lance Pioch
bcbacb47cd Fix #606 - Prevent database hosts bulk selection if host has any databases (#640)
* Prevent hosts with databases from being selected for bulk actions

* Add icons

* Update input to select

* Update app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php

* Add placeholder
2024-10-20 14:20:32 -04:00
Lance Pioch
e9f6fbadd4 Merge pull request #638 from pelican-dev/lance/pint-fixes
Reenable Disabled Pint Rules
2024-10-20 11:59:20 -04:00
Lance Pioch
c621d2dad5 Remove superfluous doc block 2024-10-20 11:55:47 -04:00
Lance Pioch
64943aa50c Merge branch 'main' into lance/pint-fixes 2024-10-20 11:53:10 -04:00
Lance Pioch
020e41cbbc Merge pull request #639 from pelican-dev/lance/phpstan-return-types
Enforce return and parameter types
2024-10-20 11:50:48 -04:00
Lance Pioch
e162374e15 Add return types 2024-10-20 11:41:46 -04:00
Lance Pioch
81c75f7966 Merge branch 'main' into lance/phpstan-return-types 2024-10-20 11:39:04 -04:00
Lance Pioch
2be8168468 Merge pull request #637 from pelican-dev/lance/enforce-di
Enforce Dependency Injection
2024-10-20 10:03:14 -04:00
Lance Pioch
465a372000 Merge pull request #641 from pelican-dev/lance/fix-node-create-redirect
Redirect to configuration file tab after creating the node
2024-10-20 10:00:53 -04:00
Lance Pioch
f0c536c045 Merge pull request #642 from pelican-dev/lance/fix-installer-redirect
Redirect to Admin Panel Dashboard after installer finishes
2024-10-20 10:00:37 -04:00
Lance Pioch
6a8e630444 Redirect to Admin Panel Dashboard after installer finishes 2024-10-19 22:16:55 -04:00
Lance Pioch
71aed151d9 Redirect to configuration file tab after creating the node 2024-10-19 22:11:24 -04:00
Lance Pioch
bb5955cff4 Have to make this match the trait 2024-10-19 21:19:59 -04:00
Lance Pioch
38be89a71e Pint 2024-10-19 21:16:33 -04:00
Lance Pioch
deb6603840 Revert "Add concat_space rule"
This reverts commit 96acd268be.
2024-10-19 21:14:41 -04:00
Lance Pioch
c7a307af6e Enforce return and parameter types 2024-10-19 21:02:49 -04:00
notCharles
8740f0f645 Change MaxWidth 2024-10-19 18:52:08 -04:00
Lance Pioch
466f9f7edc Add phpdoc_separation rule 2024-10-19 18:46:05 -04:00
Lance Pioch
d21740d458 Add phpdoc_align rule 2024-10-19 18:42:23 -04:00
Lance Pioch
1bf6a880fb Add nullable_type_declaration_for_default_null_value rule 2024-10-19 18:41:08 -04:00
Lance Pioch
96acd268be Add concat_space rule 2024-10-19 18:30:34 -04:00
Lance Pioch
c0a41acf1f Add class_attributes_separation 2024-10-19 18:29:44 -04:00
Lance Pioch
75e89b2d4c Prevent double ci checks 2024-10-19 17:25:02 -04:00
Lance Pioch
54ea55d426 Enforce DI 2024-10-19 17:22:03 -04:00
Lance Pioch
1aa51d15f0 Resolve this for now 2024-10-18 22:47:25 -04:00
Lance Pioch
3d8847a508 Resolve phpstan issues 2024-10-18 22:46:12 -04:00
Lance Pioch
4fa49ae915 This is now an array 2024-10-18 22:46:04 -04:00
Lance Pioch
a42cb193a3 Thank you merge conflict 2024-10-18 22:45:54 -04:00
Lance Pioch
50ffaf4d01 Fix tests 2024-10-18 22:38:06 -04:00
Lance Pioch
804ff64a71 Force register the validation rule 2024-10-18 22:27:18 -04:00
Lance Pioch
c2f6842f64 Smarter check 2024-10-18 22:17:15 -04:00
Lance Pioch
455d0543f1 Fix double test runners 2024-10-18 22:17:06 -04:00
Lance Pioch
97a4601150 Update laravel framework 2024-10-18 21:40:17 -04:00
Lance Pioch
2cc4a42905 This is now an array 2024-10-18 21:37:03 -04:00
Lance Pioch
5353d38302 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/NodeResource/Pages/CreateNode.php
#	app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
#	app/Filament/Resources/ServerResource/Pages/ListServers.php
#	app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php
#	app/Transformers/Api/Client/ServerTransformer.php
#	composer.lock
#	config/panel.php
2024-10-18 21:18:48 -04:00
Boy132
207d875df8 Fix default value for dns check on EditNode (#635) 2024-10-18 08:24:49 +02:00
Boy132
ff0215afed Add permission check to delete button on EditServer (#633) 2024-10-18 08:24:14 +02:00
Boy132
f357c9501f Auto-check eggs for update (#620)
* add command to check eggs for update

* add "update" button to ListEggs

* fix "unset"

* rename class

* add confirmation modal to update button
2024-10-15 22:54:06 +02:00
Boy132
71116e81ba Cleanup .env.example and configs (#624)
* add back some configs to add some defaults

* cleanup .env.example
2024-10-15 22:37:05 +02:00
Boy132
f2063d7506 Follow up installer fixes (#621)
* enable installer on docker first run

* add SESSION_COOKIE to compose file

* `APP_ENVIRONMENT_ONLY` is long gone

* session env vars no longer needed after #624

* set defaults to null if sqlite is selected
2024-10-15 22:36:35 +02:00
Boy132
c5c05150d8 Remove no longer needed View::share (#625)
* remove no longer needed `View::share`

* hardcode values so the old admin area doesn't break

* add todo comment
2024-10-15 22:35:59 +02:00
Michael (Parker) Parker
214eb5874f Merge pull request #626 from RMartinOscar/patch-1
Fix docker build workflow
2024-10-14 18:49:01 -04:00
MartinOscar
b14f6e1645 Update Dockerfile 2024-10-14 23:05:47 +02:00
RMartinOscar
04b251d125 Disable Caddy admin endpoint 2024-10-14 20:52:05 +00:00
MartinOscar
5f9ee09ebd Add yarn timeout & cleanup 2024-10-14 22:13:01 +02:00
Michael (Parker) Parker
2fb85f8236 Merge pull request #598 from Freddo3000/feature/docker-workflow
Add Docker build/publish action
2024-10-14 09:38:19 -04:00
Fredrik Falk
4eba5b3f7a Update .github/workflows/docker-publish.yml
Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-10-13 15:10:39 +02:00
Charles
f95ba6447c Add Filament Optimize (#619) 2024-10-12 10:58:18 -04:00
Boy132
c0eedc16e0 Update web installer (#614)
* update web installer

* make sure we have a user

* save SESSION_SECURE_COOKIE as text so it's written correctly to the .env

* set `SESSION_COOKIE` so session doesn't expire when changing the app name

* Allow enter to go to next step

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-10-12 10:19:52 -04:00
Boy132
3c5da1cd70 Replace all number_format with Number::format (#617) 2024-10-12 16:12:56 +02:00
Lance Pioch
8638e53f2b Merge pull request #601 from RMartinOscar/issue/600
Add can check to fix #600
2024-10-08 17:58:54 -04:00
Boy132
3ec90264bd Update API for roles (#611)
* remove `guard_name` from api and add id to transformer

* disallow update/ delete for root admin role via api

* disallow assigning root admin via api

* add api to remove user roles

* fix assignRoles & removeRoles
2024-10-08 23:46:28 +02:00
Boy132
e23a4a667a Fix escaping for EnvironmentWriterTrait (#610)
* fix escaping for EnvironmentWriterTrait

* remove alphaNum from app name field

* add test for `'` escaping
2024-10-08 23:46:06 +02:00
Boy132
a946669dc8 Add warning about database data to database settings command (#612) 2024-10-08 23:45:50 +02:00
MartinOscar
6a8ff1a186 Update app/Filament/Resources/ServerResource/Pages/ListServers.php
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-10-07 18:50:57 +02:00
Boy132
b003404aea Collection of small admin area changes (#604)
* enable tags for nodes

* update icon for cpu column

* disable inline for "force outgoing ip" label

* change label for database hosts resource

* add custom empty state for database hosts & api keys

* add icons to egg tabs

* fix typo

* rename node "Automatic Allocation" to avoid confusion

* run code cleanup

* remove regex for node name

* only check count for application api keys

* replace "New" with "Create"

* change sidebar width to fit "Database Hosts"
2024-10-04 01:15:08 +02:00
MartinOscar
45b73debc2 Switch to authorize
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-10-02 08:03:48 +00:00
RMartinOscar
329a3993c1 Add can check 2024-10-02 05:13:23 +00:00
Fredrik Falk
da7cba3203 Add docker-publish.yml workflow 2024-10-01 17:03:48 +02:00
Charles
6c205a744d Enable spa (#594) 2024-10-01 04:58:45 -04:00
Charles
e78f7bc054 Just some houseKeeping (#593)
* Just some houseKeeping

* ... pint
2024-10-01 04:37:21 -04:00
Boy132
12a189f585 Remove old queue worker args in docker supervisord (#596) 2024-09-30 14:43:35 +02:00
Boy132
af4cba341a Add config option to disable server descriptions for users (#581)
* add config option to disable server descriptions

* only disable server descriptions for users but not for admins

* Add ,

* invert

* unset description in server transformer if disabled

* remove testing leftover

---------

Co-authored-by: notCharles <charles@pelican.dev>
2024-09-29 00:35:57 +02:00
Lance Pioch
df88d33af4 Update pint 2024-09-21 15:45:22 -04:00
Lance Pioch
906d4a7d28 Use new migration logic 2024-09-19 13:19:47 -04:00
Lance Pioch
9ba8c1df9b Update Laravel framework 2024-09-18 22:22:08 -04:00
Lance Pioch
0a6b846230 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
#	app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php
#	app/Services/Allocations/AssignmentService.php
#	database/Seeders/eggs/minecraft/egg-bungeecord.json
#	database/Seeders/eggs/minecraft/egg-forge-minecraft.json
#	database/Seeders/eggs/minecraft/egg-paper.json
#	database/Seeders/eggs/minecraft/egg-sponge-sponge-vanilla.json
#	database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json
#	database/Seeders/eggs/rust/egg-rust.json
#	database/Seeders/eggs/source-engine/egg-counter-strike-global-offensive.json
#	database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json
#	database/Seeders/eggs/source-engine/egg-garrys-mod.json
#	database/Seeders/eggs/source-engine/egg-insurgency.json
#	database/Seeders/eggs/source-engine/egg-team-fortress2.json
#	database/Seeders/eggs/voice-servers/egg-mumble-server.json
#	database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json
2024-09-18 22:21:55 -04:00
Lance Pioch
aff9f4ea37 Don’t need this anymore 2024-07-04 18:56:28 -04:00
Lance Pioch
f2754c3cb1 Fix mappings 2024-07-04 13:50:48 -04:00
Lance Pioch
8b86707150 Wip 2024-07-04 13:11:13 -04:00
Lance Pioch
233fd50b2b Don’t include this by default 2024-07-04 10:24:46 -04:00
Lance Pioch
3a76fb1c79 Revert "Add custom component"
This reverts commit 0f798e5edb.
2024-07-03 22:23:18 -04:00
Lance Pioch
e76630b7f3 Merge branch 'issue/68' of github.com:pelican-dev/panel into issue/68 2024-07-03 12:14:09 -04:00
Lance Pioch
0f798e5edb Add custom component 2024-07-03 12:13:46 -04:00
Lance Pioch
a9c7eeddde Add the any ip address 2024-07-03 12:13:25 -04:00
Charles
11feef4f8c Update Eggs 2024-07-03 06:32:30 -04:00
notCharles
bebc410eda Update these 2024-07-02 19:36:53 -04:00
notCharles
ec0fa3c913 Revert "Also nuke this"
This reverts commit 4574821ed8.
2024-07-02 18:32:32 -04:00
notCharles
4574821ed8 Also nuke this 2024-07-02 16:50:39 -04:00
Lance Pioch
d71b1a4710 Merge branch 'issue/68' of github.com:pelican-dev/panel into issue/68 2024-07-02 14:39:57 -04:00
Charles
d9922e86f2 This has annoyed me... 2024-07-02 14:30:11 -04:00
Lance Pioch
9d9e4adbbd Don’t need this anymore 2024-07-02 14:27:19 -04:00
Lance Pioch
6b104e3331 Allow the server port to be selected if it also may only exist in the config 2024-07-02 14:03:05 -04:00
Lance Pioch
f2eca17480 Use constants 2024-07-02 11:50:46 -04:00
Lance Pioch
4c41e659b5 Add main server port and default 2024-07-01 15:13:20 -04:00
Lance Pioch
6238d6dd08 Switch to port rule 2024-07-01 15:12:33 -04:00
Lance Pioch
c45e4edcf6 Allow port rule to be optional 2024-07-01 15:12:03 -04:00
Lance Pioch
4273880126 Better helper text 2024-07-01 15:11:56 -04:00
Lance Pioch
d86843977b Run pint 2024-07-01 14:55:17 -04:00
Lance Pioch
5b468c21ae Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php
#	app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php
#	app/Filament/Resources/EggResource/RelationManagers/ServersRelationManager.php
#	app/Filament/Resources/NodeResource.php
#	app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
2024-07-01 14:54:12 -04:00
Lance Pioch
68ef0a1d0a Fix these 2024-06-26 22:03:26 -04:00
Lance Pioch
f6122f919a Rename this because order matters 2024-06-26 22:03:23 -04:00
Lance Pioch
4f10ec2c20 Fix name 2024-06-26 21:56:01 -04:00
Lance Pioch
45fcc2a09a Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php
#	app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
#	app/Filament/Resources/ServerResource/Pages/ListServers.php
#	app/Http/Requests/Admin/Node/AllocationFormRequest.php
#	app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php
#	app/Models/AuditLog.php
#	app/Models/Server.php
2024-06-26 21:52:05 -04:00
Lance Pioch
19c7b4d044 Clean up 2024-06-26 21:44:06 -04:00
Lance Pioch
7c8b204d13 Remove network tab 2024-06-26 21:42:57 -04:00
Lance Pioch
343a5b81bc Almost done 2024-06-26 21:38:18 -04:00
Lance Pioch
755632f9d5 Wip 2024-06-23 22:32:37 -04:00
Lance Pioch
e7ee86a914 Fix whoopsie 2024-06-23 22:31:41 -04:00
Lance Pioch
eb0bad82e6 Wip 2024-06-22 11:30:08 -04:00
Lance Pioch
48fd3cc84e Add restore to simplify form 2024-06-18 10:59:56 -04:00
Lance Pioch
d4484f5254 Handle nulls 2024-06-18 10:59:50 -04:00
Lance Pioch
958e8fac8a Simplify states and statuses and resolve #362 2024-06-17 18:18:48 -04:00
Lance Pioch
7986505b99 Don’t report status anymore 2024-06-17 18:15:56 -04:00
Lance Pioch
ba5b81cf2d Show localhost 2024-06-17 18:15:45 -04:00
Lance Pioch
32018399b6 Add server io weight default 2024-06-17 18:15:35 -04:00
Lance Pioch
48f4c35d0b Can be null apparently 2024-06-17 11:55:32 -04:00
Lance Pioch
f699fd5459 Make ports into badges 2024-06-17 11:46:58 -04:00
Lance Pioch
05573f64dd Group servers 2024-06-17 11:46:40 -04:00
Lance Pioch
3fa714c7e3 Refactor 2024-06-17 10:48:05 -04:00
Lance Pioch
69acc48b5e Use constants 2024-06-17 10:47:55 -04:00
Lance Pioch
9d9720a5a2 Start servers by default 2024-06-17 10:47:43 -04:00
Lance Pioch
ff261f9c99 Realism 2024-06-16 13:07:12 -04:00
Lance Pioch
c7bea4f024 This was wrong but somehow worked in sqlite, wut 2024-06-16 12:30:42 -04:00
Lance Pioch
738707b251 Add validation rule 2024-06-16 12:30:28 -04:00
Lance Pioch
a8699704de Fix return 2024-06-16 11:56:21 -04:00
Lance Pioch
d9dc932e07 Remove unused import 2024-06-16 11:55:54 -04:00
Lance Pioch
f57232bc23 Fix tests 2024-06-16 11:50:08 -04:00
Lance Pioch
b24ff8bb26 Unwrap transaction 2024-06-15 07:27:17 -04:00
Lance Pioch
eff8e509ef Wrap in transaction 2024-06-15 07:19:01 -04:00
Lance Pioch
6976fa8989 Try again 2024-06-15 06:57:11 -04:00
Lance Pioch
2b58160da9 Drop it 2024-06-15 06:04:20 -04:00
Lance Pioch
44e0dd3e09 Remove final refs 2024-06-15 06:01:40 -04:00
Lance Pioch
8ea57bc46b Remove unused audit logs 2024-06-15 05:53:29 -04:00
Lance Pioch
459d90e8d1 Revert this 2024-06-15 05:44:53 -04:00
Lance Pioch
a97341f6f2 Cast to int 2024-06-15 05:40:25 -04:00
Lance Pioch
375a64a38e Remove these relationships 2024-06-15 05:40:21 -04:00
Lance Pioch
7c25fc2a9d Revert these doc blocks 2024-06-15 05:39:53 -04:00
Lance Pioch
1a26f5ce9e This goes first 2024-06-15 05:34:56 -04:00
Lance Pioch
b47f40bd13 Remove debug 2024-06-15 05:24:57 -04:00
Lance Pioch
bcb7240ed2 Pint fixes 2024-06-15 05:24:19 -04:00
Lance Pioch
405aa857b1 Reset these for now 2024-06-15 05:23:33 -04:00
Lance Pioch
0bd2935885 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/EggResource/RelationManagers/ServersRelationManager.php
#	app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/ListServers.php
#	app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php
#	app/Models/Allocation.php
#	app/Models/ApiKey.php
#	app/Models/Server.php
#	app/Models/User.php
2024-06-15 05:21:58 -04:00
Lance Pioch
4cba1540ac Add endpoints 2024-06-15 05:14:54 -04:00
Lance Pioch
30051ab0d7 Add tooltips for versions 2024-06-15 05:14:30 -04:00
Lance Pioch
e15d515f71 Wip 2024-06-14 07:54:07 -04:00
Lance Pioch
36e2fa8e2b Wip 2024-06-13 11:31:58 -04:00
Lance Pioch
510ae3c0df Swap this for now 2024-06-10 21:38:28 -04:00
Lance Pioch
f5edb34873 Some filament changes for ports 2024-06-10 21:32:46 -04:00
Lance Pioch
0895bd2be5 Remove relation managers 2024-06-10 20:35:36 -04:00
Lance Pioch
17bc3de0d0 Rename translation keys 2024-06-10 20:34:46 -04:00
Lance Pioch
4319f24f51 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Models/Node.php
2024-06-09 15:42:49 -04:00
Lance Pioch
9ad113bc61 Move these 2024-06-09 14:18:19 -04:00
Lance Pioch
beadce96f6 Wip 2024-06-09 08:20:31 -04:00
Lance Pioch
b1d7d210fc Allow startup command to change with server variable 2024-06-07 01:00:00 -04:00
Lance Pioch
32e96dc0a6 Wip 2024-06-06 15:49:36 -04:00
Lance Pioch
b16a11c365 No longer reserved 2024-06-06 15:49:33 -04:00
Lance Pioch
81f218ddc9 Skip port variables down below 2024-06-06 15:49:23 -04:00
Lance Pioch
be6f79521e Wip 2024-06-04 16:34:54 -04:00
Lance Pioch
551175862e Better kebab names 2024-06-04 15:38:45 -04:00
Lance Pioch
dbad5ae9c7 WIP 2024-06-04 11:40:19 -04:00
Lance Pioch
768a45bbb8 More updates 2024-06-02 21:55:13 -04:00
Lance Pioch
bbe09ced1d Add some new options for ports 2024-06-02 16:58:32 -04:00
Lance Pioch
2e7c534a3b Disable this if there’s no egg 2024-06-02 16:58:21 -04:00
Lance Pioch
71684dc517 Pint fix 2024-06-02 01:38:42 -04:00
Lance Pioch
4dbb55059d Remove auto generated properties 2024-06-02 01:38:30 -04:00
Lance Pioch
f480a271b3 Simplify logic 2024-06-02 01:27:04 -04:00
Lance Pioch
9c81c0ce18 Use public member 2024-06-02 01:25:48 -04:00
Lance Pioch
b220c582cc Rearrange these 2024-06-02 01:22:33 -04:00
Lance Pioch
29f8ac625a This is for the api 2024-06-02 01:22:21 -04:00
Lance Pioch
5b6c462943 This isn’t typically used for a brand new server 2024-06-02 01:22:12 -04:00
258 changed files with 2200 additions and 4984 deletions

View File

@@ -1,32 +1,7 @@
APP_ENV=production
APP_DEBUG=false
APP_KEY=
APP_TIMEZONE=UTC
APP_URL=http://panel.test
APP_INSTALLED=false
APP_TIMEZONE=UTC
APP_LOCALE=en
LOG_CHANNEL=daily
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
CACHE_STORE=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
MAIL_MAILER=log
MAIL_HOST=smtp.example.com
MAIL_PORT=25
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="Pelican Admin"
# Set this to your domain to prevent it defaulting to 'localhost', causing mail servers such as Gmail to reject your mail
# MAIL_EHLO_DOMAIN=panel.example.com
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

View File

@@ -21,6 +21,9 @@ else
echo -e "APP_KEY exists in environment, using that."
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
fi
## enable installer
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
fi
mkdir /pelican-data/database
@@ -38,6 +41,9 @@ fi
echo -e "Migrating Database"
php artisan migrate --force
echo -e "Optimizing Filament"
php artisan filament:optimize
## start cronjobs for the queue
echo -e "Starting cron jobs."
crond -L /var/log/crond -l 5

View File

@@ -25,7 +25,7 @@ autostart=true
autorestart=true
[program:queue-worker]
command=/usr/local/bin/php /var/www/html/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
command=/usr/local/bin/php /var/www/html/artisan queue:work --tries=3
user=www-data
autostart=true
autorestart=true
@@ -36,4 +36,4 @@ autostart=%(ENV_SUPERVISORD_CADDY)s
autorestart=%(ENV_SUPERVISORD_CADDY)s
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stderr_events_enabled=true

View File

@@ -3,10 +3,8 @@ name: Tests
on:
push:
branches:
- '**'
- main
pull_request:
branches:
- '**'
jobs:
mysql:

82
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
name: Docker
on:
push:
branches:
- main
release:
types:
- published
jobs:
build-and-push:
name: Build and Push
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
if: "!contains(github.ref, 'main') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
steps:
- name: Code checkout
uses: actions/checkout@v4
- name: Docker metadata
id: docker_meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/pelican-dev/panel
flavor: |
latest=false
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
type=ref,event=tag
type=ref,event=branch
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Build Information
id: build_info
run: |
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Build and Push (tag)
uses: docker/build-push-action@v5
if: "github.event_name == 'release' && github.event.action == 'published'"
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.build_info.outputs.version_tag }}
labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }}
- name: Build and Push (main)
uses: docker/build-push-action@v5
if: "github.event_name == 'push' && contains(github.ref, 'main')"
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=dev-${{ steps.build_info.outputs.short_sha }}
labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,4 +1,5 @@
{
admin off
email {$ADMIN_EMAIL}
}

View File

@@ -7,7 +7,9 @@ WORKDIR /build
COPY . ./
RUN yarn install --frozen-lockfile && yarn run build:production
RUN yarn config set network-timeout 300000 \
&& yarn install --frozen-lockfile \
&& yarn run build:production
FROM php:8.3-fpm-alpine
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
@@ -36,8 +38,7 @@ RUN touch .env
RUN composer install --no-dev --optimize-autoloader
# Set file permissions
RUN chmod -R 755 /var/www/html/storage \
&& chmod -R 755 /var/www/html/bootstrap/cache
RUN chmod -R 755 storage bootstrap/cache
# Add scheduler to cron
RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -
@@ -49,8 +50,7 @@ RUN cp .github/docker/supervisord.conf /etc/supervisord.conf && \
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1
EXPOSE 80:2019
EXPOSE 443
EXPOSE 80 443
VOLUME /pelican-data

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Casts;
use App\Models\Objects\Endpoint;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
class EndpointCollection implements Castable
{
public static function castUsing(array $arguments)
{
return new class() implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
if (!isset($attributes[$key])) {
return new Collection();
}
$data = json_decode($attributes[$key], true);
return (new Collection($data))->map(function ($value) {
return new Endpoint($value);
});
}
public function set($model, $key, $value, $attributes)
{
if (!is_array($value) && !$value instanceof Collection) {
return new Collection();
}
if (!$value instanceof Collection) {
$value = new Collection($value);
}
return [
'ports' => $value->toJson(),
];
}
};
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Console\Commands\Egg;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use Exception;
use Illuminate\Console\Command;
class CheckEggUpdatesCommand extends Command
{
protected $signature = 'p:egg:check-updates';
public function handle(EggExporterService $exporterService): void
{
$eggs = Egg::all();
foreach ($eggs as $egg) {
try {
if (is_null($egg->update_url)) {
$this->comment("{$egg->name}: Skipping (no update url set)");
continue;
}
$currentJson = json_decode($exporterService->handle($egg->id));
unset($currentJson->exported_at);
$updatedJson = json_decode(file_get_contents($egg->update_url));
unset($updatedJson->exported_at);
if (md5(json_encode($currentJson)) === md5(json_encode($updatedJson))) {
$this->info("{$egg->name}: Up-to-date");
cache()->put("eggs.{$egg->uuid}.update", false, now()->addHour());
} else {
$this->warn("{$egg->name}: Found update");
cache()->put("eggs.{$egg->uuid}.update", true, now()->addHour());
}
} catch (Exception $exception) {
$this->error("{$egg->name}: Error ({$exception->getMessage()})");
}
}
}
}

View File

@@ -2,20 +2,14 @@
namespace App\Console\Commands\Environment;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command
{
use EnvironmentWriterTrait;
protected $description = 'Configure basic environment settings for the Panel.';
protected $signature = 'p:environment:setup
{--url= : The URL that this Panel is running on.}';
protected array $variables = [];
protected $signature = 'p:environment:setup';
public function handle(): void
{
@@ -30,21 +24,6 @@ class AppSettingsCommand extends Command
Artisan::call('key:generate');
}
$this->variables['APP_TIMEZONE'] = 'UTC';
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
'Application URL',
config('app.url', 'https://example.com')
);
// Make sure session cookies are set as "secure" when using HTTPS
if (str_starts_with($this->variables['APP_URL'], 'https://')) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
}
$this->comment('Writing variables to .env file');
$this->writeToEnvironment($this->variables);
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
Artisan::call('filament:optimize');
}
}

View File

@@ -42,6 +42,13 @@ class DatabaseSettingsCommand extends Command
*/
public function handle(): int
{
$this->error('Changing the database driver will NOT move any database data!');
$this->error('Please make sure you made a database backup first!');
$this->error('After changing the driver you will have to manually move the old data to the new database.');
if (!$this->confirm('Do you want to continue?')) {
return 1;
}
$selected = config('database.default', 'sqlite');
$this->variables['DB_CONNECTION'] = $this->option('driver') ?? $this->choice(
'Database Driver',

View File

@@ -70,7 +70,7 @@ class EmailSettingsCommand extends Command
/**
* Handle variables for SMTP driver.
*/
private function setupSmtpDriverVariables()
private function setupSmtpDriverVariables(): void
{
$this->variables['MAIL_HOST'] = $this->option('host') ?? $this->ask(
trans('command/messages.environment.mail.ask_smtp_host'),
@@ -101,7 +101,7 @@ class EmailSettingsCommand extends Command
/**
* Handle variables for mailgun driver.
*/
private function setupMailgunDriverVariables()
private function setupMailgunDriverVariables(): void
{
$this->variables['MAILGUN_DOMAIN'] = $this->option('host') ?? $this->ask(
trans('command/messages.environment.mail.ask_mailgun_domain'),
@@ -122,7 +122,7 @@ class EmailSettingsCommand extends Command
/**
* Handle variables for mandrill driver.
*/
private function setupMandrillDriverVariables()
private function setupMandrillDriverVariables(): void
{
$this->variables['MANDRILL_SECRET'] = $this->option('password') ?? $this->ask(
trans('command/messages.environment.mail.ask_mandrill_secret'),
@@ -133,7 +133,7 @@ class EmailSettingsCommand extends Command
/**
* Handle variables for postmark driver.
*/
private function setupPostmarkDriverVariables()
private function setupPostmarkDriverVariables(): void
{
$this->variables['MAIL_DRIVER'] = 'smtp';
$this->variables['MAIL_HOST'] = 'smtp.postmarkapp.com';

View File

@@ -51,7 +51,7 @@ class ProcessRunnableCommand extends Command
* never throw an exception out, otherwise you'll end up killing the entire run group causing
* any other schedules to not process correctly.
*/
protected function processSchedule(Schedule $schedule)
protected function processSchedule(Schedule $schedule): void
{
if ($schedule->tasks->isEmpty()) {
return;

View File

@@ -178,7 +178,7 @@ class UpgradeCommand extends Command
$this->info(__('commands.upgrade.success'));
}
protected function withProgress(ProgressBar $bar, \Closure $callback)
protected function withProgress(ProgressBar $bar, \Closure $callback): void
{
$bar->clear();
$callback();

View File

@@ -2,15 +2,16 @@
namespace App\Console;
use App\Console\Commands\Egg\CheckEggUpdatesCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand;
class Kernel extends ConsoleKernel
{
@@ -35,6 +36,7 @@ class Kernel extends ConsoleKernel
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
$schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();

View File

@@ -4,6 +4,8 @@ namespace App\Exceptions;
use Exception;
use Filament\Notifications\Notification;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
@@ -14,8 +16,11 @@ use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class DisplayException extends PanelException implements HttpExceptionInterface
{
public const LEVEL_DEBUG = 'debug';
public const LEVEL_INFO = 'info';
public const LEVEL_WARNING = 'warning';
public const LEVEL_ERROR = 'error';
/**
@@ -46,7 +51,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
* and then redirecting them back to the page that they came from. If the
* request originated from an API hit, return the error in JSONAPI spec format.
*/
public function render(Request $request)
public function render(Request $request): bool|RedirectResponse|JsonResponse
{
if ($request->is('livewire/update')) {
Notification::make()
@@ -55,13 +60,14 @@ class DisplayException extends PanelException implements HttpExceptionInterface
->danger()
->send();
return;
return false;
}
if ($request->expectsJson()) {
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
}
// @phpstan-ignore-next-line
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
return redirect()->back()->withInput();
@@ -73,10 +79,10 @@ class DisplayException extends PanelException implements HttpExceptionInterface
*
* @throws \Throwable
*/
public function report()
public function report(): void
{
if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) {
return null;
return;
}
try {
@@ -85,6 +91,6 @@ class DisplayException extends PanelException implements HttpExceptionInterface
throw $this->getPrevious();
}
return $logger->{$this->getErrorLevel()}($this->getPrevious());
$logger->{$this->getErrorLevel()}($this->getPrevious());
}
}

View File

@@ -114,7 +114,7 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
*
* @throws \Throwable
*/
@@ -140,7 +140,7 @@ class Handler extends ExceptionHandler
* Transform a validation exception into a consistent format to be returned for
* calls to the API.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
*/
public function invalidJson($request, ValidationException $exception): JsonResponse
{
@@ -236,7 +236,7 @@ class Handler extends ExceptionHandler
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
*/
protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse
{
@@ -273,6 +273,7 @@ class Handler extends ExceptionHandler
*/
public static function toArray(\Throwable $e): array
{
// @phpstan-ignore-next-line
return (new self(app()))->convertExceptionToArray($e);
}
}

View File

@@ -10,7 +10,7 @@ class HttpForbiddenException extends HttpException
/**
* HttpForbiddenException constructor.
*/
public function __construct(string $message = null, \Throwable $previous = null)
public function __construct(?string $message = null, ?\Throwable $previous = null)
{
parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous);
}

View File

@@ -12,7 +12,7 @@ class ServerStateConflictException extends ConflictHttpException
* Exception thrown when the server is in an unsupported state for API access or
* certain operations within the codebase.
*/
public function __construct(Server $server, \Throwable $previous = null)
public function __construct(Server $server, ?\Throwable $previous = null)
{
$message = 'This server is currently in an unsupported state, please try again later.';
if ($server->isSuspended()) {

View File

@@ -11,7 +11,7 @@ class TwoFactorAuthRequiredException extends HttpException implements HttpExcept
/**
* TwoFactorAuthRequiredException constructor.
*/
public function __construct(\Throwable $previous = null)
public function __construct(?\Throwable $previous = null)
{
parent::__construct(Response::HTTP_BAD_REQUEST, 'Two-factor authentication is required on this account in order to access this endpoint.', $previous);
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\PanelException;
class AllocationDoesNotBelongToServerException extends PanelException
{
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class AutoAllocationNotEnabledException extends DisplayException
{
/**
* AutoAllocationNotEnabledException constructor.
*/
public function __construct()
{
parent::__construct(
'Server auto-allocation is not enabled for this instance.'
);
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class CidrOutOfRangeException extends DisplayException
{
/**
* CidrOutOfRangeException constructor.
*/
public function __construct()
{
parent::__construct(trans('exceptions.allocations.cidr_out_of_range'));
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class InvalidPortMappingException extends DisplayException
{
/**
* InvalidPortMappingException constructor.
*/
public function __construct(mixed $port)
{
parent::__construct(trans('exceptions.allocations.invalid_mapping', ['port' => $port]));
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class NoAutoAllocationSpaceAvailableException extends DisplayException
{
/**
* NoAutoAllocationSpaceAvailableException constructor.
*/
public function __construct()
{
parent::__construct(
'Cannot assign additional allocation: no more space available on node.'
);
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class PortOutOfRangeException extends DisplayException
{
/**
* PortOutOfRangeException constructor.
*/
public function __construct()
{
parent::__construct(trans('exceptions.allocations.port_out_of_range'));
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class ServerUsingAllocationException extends DisplayException
{
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class TooManyPortsInRangeException extends DisplayException
{
/**
* TooManyPortsInRangeException constructor.
*/
public function __construct()
{
parent::__construct(trans('exceptions.allocations.too_many_ports'));
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableAllocationException extends DisplayException
{
}

View File

@@ -10,7 +10,7 @@ class ServiceLimitExceededException extends DisplayException
* Exception thrown when something goes over a defined limit, such as allocated
* ports, tasks, databases, etc.
*/
public function __construct(string $message, \Throwable $previous = null)
public function __construct(string $message, ?\Throwable $previous = null)
{
parent::__construct($message, $previous, self::LEVEL_WARNING);
}

View File

@@ -7,6 +7,7 @@ use App\Exceptions\DisplayException;
class TwoFactorAuthenticationTokenInvalid extends DisplayException
{
public string $title = 'Invalid 2FA Code';
public string $icon = 'tabler-2fa';
public function __construct()

View File

@@ -34,7 +34,7 @@ class BackupManager
/**
* Returns a backup adapter instance.
*/
public function adapter(string $name = null): FilesystemAdapter
public function adapter(?string $name = null): FilesystemAdapter
{
return $this->get($name ?: $this->getDefaultAdapter());
}
@@ -145,7 +145,7 @@ class BackupManager
/**
* Unset the given adapter instances.
*
* @param string|string[] $adapter
* @param string|string[] $adapter
*/
public function forget(array|string $adapter): self
{

View File

@@ -7,7 +7,9 @@ use App\Models\DatabaseHost;
class DynamicDatabaseConnection
{
public const DB_CHARSET = 'utf8';
public const DB_COLLATION = 'utf8_unicode_ci';
public const DB_DRIVER = 'mysql';
/**

View File

@@ -4,17 +4,17 @@ namespace App\Extensions\Themes;
class Theme
{
public function js($path): string
public function js(string $path): string
{
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
}
public function css($path): string
public function css(string $path): string
{
return sprintf('<link media="all" type="text/css" rel="stylesheet" href="%s"/>' . PHP_EOL, $this->getUrl($path));
}
protected function getUrl($path): string
protected function getUrl(string $path): string
{
return '/themes/panel/' . ltrim($path, '/');
}

View File

@@ -28,16 +28,20 @@ class Dashboard extends Page
public string $activeTab = 'nodes';
private SoftwareVersionService $softwareVersionService;
public function mount(SoftwareVersionService $softwareVersionService): void
{
$this->softwareVersionService = $softwareVersionService;
}
public function getViewData(): array
{
/** @var SoftwareVersionService $softwareVersionService */
$softwareVersionService = app(SoftwareVersionService::class);
return [
'inDevelopment' => config('app.version') === 'canary',
'version' => $softwareVersionService->versionData()['version'],
'latestVersion' => $softwareVersionService->getPanel(),
'isLatest' => $softwareVersionService->isLatestPanel(),
'version' => $this->softwareVersionService->versionData()['version'],
'latestVersion' => $this->softwareVersionService->getPanel(),
'isLatest' => $this->softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
'nodesCount' => Node::query()->count(),
@@ -67,7 +71,7 @@ class Dashboard extends Page
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url($softwareVersionService->getDonations(), true)
->url($this->softwareVersionService->getDonations(), true)
->color('success'),
],
'helpActions' => [

View File

@@ -2,7 +2,9 @@
namespace App\Filament\Pages\Installer;
use App\Filament\Pages\Dashboard;
use App\Filament\Pages\Installer\Steps\AdminUserStep;
use App\Filament\Pages\Installer\Steps\CompletedStep;
use App\Filament\Pages\Installer\Steps\DatabaseStep;
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
use App\Filament\Pages\Installer\Steps\RedisStep;
@@ -12,15 +14,17 @@ use App\Services\Users\UserCreationService;
use App\Traits\CheckMigrationsTrait;
use App\Traits\EnvironmentWriterTrait;
use Exception;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Wizard;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\SimplePage;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Exceptions\Halt;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
@@ -32,56 +36,46 @@ class PanelInstaller extends SimplePage implements HasForms
{
use CheckMigrationsTrait;
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;
public $data = [];
public array $data = [];
protected static string $view = 'filament.pages.installer';
private User $user;
public function getMaxWidth(): MaxWidth|string
{
return MaxWidth::SevenExtraLarge;
}
public static function show(): bool
public static function isInstalled(): bool
{
if (User::count() <= 0) {
return true;
}
if (config('panel.client_features.installer.enabled')) {
return true;
}
return false;
// This defaults to true so existing panels count as "installed"
return env('APP_INSTALLED', true);
}
public function mount()
public function mount(): void
{
abort_unless(self::show(), 404);
abort_if(self::isInstalled(), 404);
$this->form->fill();
}
public function dehydrate(): void
{
Artisan::call('config:clear');
Artisan::call('cache:clear');
}
protected function getFormSchema(): array
{
return [
Wizard::make([
RequirementsStep::make(),
EnvironmentStep::make(),
DatabaseStep::make(),
RedisStep::make()
->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'),
AdminUserStep::make(),
EnvironmentStep::make($this),
DatabaseStep::make($this),
RedisStep::make($this)
->hidden(fn (Get $get) => $get('env_general.SESSION_DRIVER') != 'redis' && $get('env_general.QUEUE_CONNECTION') != 'redis' && $get('env_general.CACHE_STORE') != 'redis'),
AdminUserStep::make($this),
CompletedStep::make(),
])
->persistStepInQueryString()
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
type="submit"
@@ -100,61 +94,89 @@ class PanelInstaller extends SimplePage implements HasForms
return 'data';
}
protected function hasUnsavedDataChangesAlert(): bool
public function submit(): RedirectResponse
{
return true;
// Disable installer
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
// Login user
$this->user ??= User::all()->filter(fn ($user) => $user->isRootAdmin())->first();
auth()->guard()->login($this->user, true);
// Redirect to admin panel
return redirect(Dashboard::getUrl());
}
public function submit()
public function writeToEnv(string $key): void
{
try {
$inputs = $this->form->getState();
// Write variables to .env file
$variables = array_get($inputs, 'env');
$variables = array_get($this->data, $key);
$this->writeToEnvironment($variables);
// Clear config cache
Artisan::call('config:clear');
// Run migrations
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
'--database' => $variables['DB_CONNECTION'],
]);
if (!$this->hasCompletedMigrations()) {
throw new Exception('Migrations didn\'t run successfully. Double check your database configuration.');
}
// Create first admin user
$userData = array_get($inputs, 'user');
$userData['root_admin'] = true;
$user = app(UserCreationService::class)->handle($userData);
// Install setup complete
$this->writeToEnvironment(['APP_INSTALLER' => 'false']);
$this->rememberData();
Notification::make()
->title('Successfully Installed')
->success()
->send();
auth()->loginUsingId($user->id);
return redirect('/admin');
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Installation Failed')
->title('Could not write to .env file')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while writing .env file');
}
Artisan::call('config:clear');
}
public function runMigrations(string $driver): void
{
try {
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
'--database' => $driver,
]);
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Migrations failed')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while running migrations');
}
if (!$this->hasCompletedMigrations()) {
Notification::make()
->title('Migrations failed')
->danger()
->persistent()
->send();
throw new Halt('Migrations failed');
}
}
public function createAdminUser(UserCreationService $userCreationService): void
{
try {
$userData = array_get($this->data, 'user');
$userData['root_admin'] = true;
$this->user = $userCreationService->handle($userData);
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Could not create admin user')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while creating admin user');
}
}
}

View File

@@ -2,12 +2,14 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Services\Users\UserCreationService;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
class AdminUserStep
{
public static function make(): Step
public static function make(PanelInstaller $installer): Step
{
return Step::make('user')
->label('Admin User')
@@ -26,6 +28,7 @@ class AdminUserStep
->required()
->password()
->revealable(),
]);
])
->afterValidation(fn (UserCreationService $service) => $installer->createAdminUser($service));
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class CompletedStep
{
public static function make(): Step
{
return Step::make('complete')
->label('Setup complete')
->schema([
Placeholder::make('')
->content(new HtmlString('The setup is nearly complete!<br>As last step you need to create a new cronjob that runs every minute to process specific tasks, such as session cleanup and scheduled tasks, and also create a queue worker.')),
TextInput::make('crontab')
->label(new HtmlString('Run the following command to setup your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
->disabled()
->hintAction(CopyAction::make())
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -'),
TextInput::make('queueService')
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
->disabled()
->hintAction(CopyAction::make())
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service'),
Placeholder::make('')
->content('After you finished these two last tasks you can click on "Finish" and use your new panel! Have fun!'),
]);
}
}

View File

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

View File

@@ -2,14 +2,17 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Traits\EnvironmentWriterTrait;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Set;
class EnvironmentStep
{
use EnvironmentWriterTrait;
public const CACHE_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
@@ -17,14 +20,14 @@ class EnvironmentStep
public const SESSION_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
'database' => 'Database',
'cookie' => 'Cookie',
'redis' => 'Redis',
];
public const QUEUE_DRIVERS = [
'sync' => 'Sync',
'database' => 'Database',
'sync' => 'Sync',
'redis' => 'Redis',
];
@@ -34,30 +37,30 @@ class EnvironmentStep
'mysql' => 'MySQL',
];
public static function make(): Step
public static function make(PanelInstaller $installer): Step
{
return Step::make('environment')
->label('Environment')
->columns()
->schema([
TextInput::make('env.APP_NAME')
TextInput::make('env_general.APP_NAME')
->label('App Name')
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the Name of your Panel.')
->required()
->default(config('app.name')),
TextInput::make('env.APP_URL')
TextInput::make('env_general.APP_URL')
->label('App URL')
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the URL you access your Panel from.')
->required()
->default(config('app.url'))
->default(url(''))
->live()
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))),
Toggle::make('env.SESSION_SECURE_COOKIE')
->afterStateUpdated(fn ($state, Set $set) => $set('env_general.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://') ? 'true' : 'false')),
TextInput::make('env_general.SESSION_SECURE_COOKIE')
->hidden()
->default(env('SESSION_SECURE_COOKIE')),
ToggleButtons::make('env.CACHE_STORE')
->default(str_starts_with(url(''), 'https://') ? 'true' : 'false'),
ToggleButtons::make('env_general.CACHE_STORE')
->label('Cache Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
@@ -65,7 +68,7 @@ class EnvironmentStep
->inline()
->options(self::CACHE_DRIVERS)
->default(config('cache.default', 'file')),
ToggleButtons::make('env.SESSION_DRIVER')
ToggleButtons::make('env_general.SESSION_DRIVER')
->label('Session Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
@@ -73,15 +76,15 @@ class EnvironmentStep
->inline()
->options(self::SESSION_DRIVERS)
->default(config('session.driver', 'file')),
ToggleButtons::make('env.QUEUE_CONNECTION')
ToggleButtons::make('env_general.QUEUE_CONNECTION')
->label('Queue Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Sync" or "Database".')
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->required()
->inline()
->options(self::QUEUE_DRIVERS)
->default(config('queue.default', 'database')),
ToggleButtons::make('env.DB_CONNECTION')
ToggleButtons::make('env_general.DB_CONNECTION')
->label('Database Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
@@ -89,6 +92,7 @@ class EnvironmentStep
->inline()
->options(self::DATABASE_DRIVERS)
->default(config('database.default', 'sqlite')),
]);
])
->afterValidation(fn () => $installer->writeToEnv('env_general'));
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Traits\EnvironmentWriterTrait;
use Exception;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
@@ -12,30 +14,32 @@ use Illuminate\Support\Facades\Redis;
class RedisStep
{
public static function make(): Step
use EnvironmentWriterTrait;
public static function make(PanelInstaller $installer): Step
{
return Step::make('redis')
->label('Redis')
->columns()
->schema([
TextInput::make('env.REDIS_HOST')
TextInput::make('env_redis.REDIS_HOST')
->label('Redis Host')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
->required()
->default(config('database.redis.default.host')),
TextInput::make('env.REDIS_PORT')
TextInput::make('env_redis.REDIS_PORT')
->label('Redis Port')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your redis server.')
->required()
->default(config('database.redis.default.port')),
TextInput::make('env.REDIS_USERNAME')
TextInput::make('env_redis.REDIS_USERNAME')
->label('Redis Username')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your redis user. Can be empty')
->default(config('database.redis.default.username')),
TextInput::make('env.REDIS_PASSWORD')
TextInput::make('env_redis.REDIS_PASSWORD')
->label('Redis Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.')
@@ -43,25 +47,36 @@ class RedisStep
->revealable()
->default(config('database.redis.default.password')),
])
->afterValidation(function (Get $get) {
try {
config()->set('database.redis._panel_install_test', [
'host' => $get('env.REDIS_HOST'),
'username' => $get('env.REDIS_USERNAME'),
'password' => $get('env.REDIS_PASSWORD'),
'port' => $get('env.REDIS_PORT'),
]);
Redis::connection('_panel_install_test')->command('ping');
} catch (Exception $exception) {
Notification::make()
->title('Redis connection failed')
->body($exception->getMessage())
->danger()
->send();
->afterValidation(function (Get $get) use ($installer) {
if (!self::testConnection($get('env_redis.REDIS_HOST'), $get('env_redis.REDIS_PORT'), $get('env_redis.REDIS_USERNAME'), $get('env_redis.REDIS_PASSWORD'))) {
throw new Halt('Redis connection failed');
}
$installer->writeToEnv('env_redis');
});
}
private static function testConnection(string $host, string $port, string $username, string $password): bool
{
try {
config()->set('database.redis._panel_install_test', [
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
]);
Redis::connection('_panel_install_test')->command('ping');
} catch (Exception $exception) {
Notification::make()
->title('Redis connection failed')
->body($exception->getMessage())
->danger()
->send();
return false;
}
return true;
}
}

View File

@@ -10,18 +10,20 @@ use Filament\Support\Exceptions\Halt;
class RequirementsStep
{
public const MIN_PHP_VERSION = '8.2';
public static function make(): Step
{
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0;
$correctPhpVersion = version_compare(PHP_VERSION, self::MIN_PHP_VERSION) >= 0;
$fields = [
Section::make('PHP Version')
->description('8.2 or newer')
->description(self::MIN_PHP_VERSION . ' or newer')
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
->iconColor($correctPhpVersion ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'),
->content('Your PHP Version is ' . PHP_VERSION . '.'),
]),
];
@@ -80,7 +82,7 @@ class RequirementsStep
->danger()
->send();
throw new Halt();
throw new Halt('Some requirements are missing');
}
});
}

View File

@@ -38,6 +38,7 @@ class Settings extends Page implements HasForms
use InteractsWithHeaderActions;
protected static ?string $navigationIcon = 'tabler-settings';
protected static ?string $navigationGroup = 'Advanced';
protected static string $view = 'filament.pages.settings';
@@ -92,7 +93,6 @@ class Settings extends Page implements HasForms
TextInput::make('APP_NAME')
->label('App Name')
->required()
->alphaNum()
->default(env('APP_NAME', 'Pelican')),
TextInput::make('APP_FAVICON')
->label('App Favicon')
@@ -522,6 +522,25 @@ class Settings extends Page implements HasForms
->suffix('Requests Per Minute')
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
]),
Section::make('Server')
->description('Settings for Servers.')
->columns()
->collapsible()
->collapsed()
->schema([
Toggle::make('PANEL_EDITABLE_SERVER_DESCRIPTIONS')
->label('Allow Users to edit Server Descriptions?')
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
]),
];
}

View File

@@ -5,12 +5,16 @@ namespace App\Filament\Resources;
use App\Filament\Resources\ApiKeyResource\Pages;
use App\Models\ApiKey;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
class ApiKeyResource extends Resource
{
protected static ?string $model = ApiKey::class;
protected static ?string $label = 'API Key';
protected static ?string $navigationIcon = 'tabler-key';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
@@ -18,18 +22,11 @@ class ApiKeyResource extends Resource
return static::getModel()::where('key_type', '2')->count() ?: null;
}
public static function canEdit($record): bool
public static function canEdit(Model $record): bool
{
return false;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -6,6 +6,7 @@ use App\Filament\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -51,13 +52,23 @@ class ListApiKeys extends ListRecords
])
->actions([
DeleteAction::make(),
])
->emptyStateIcon('tabler-key')
->emptyStateDescription('')
->emptyStateHeading('No API Keys')
->emptyStateActions([
CreateAction::make('create')
->label('Create API Key')
->button(),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
Actions\CreateAction::make()
->label('Create API Key')
->hidden(fn () => ApiKey::where('key_type', ApiKey::TYPE_APPLICATION)->count() <= 0),
];
}
}

View File

@@ -10,9 +10,10 @@ class DatabaseHostResource extends Resource
{
protected static ?string $model = DatabaseHost::class;
protected static ?string $label = 'Databases';
protected static ?string $label = 'Database Host';
protected static ?string $navigationIcon = 'tabler-database';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
@@ -20,13 +21,6 @@ class DatabaseHostResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -3,19 +3,24 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Models\Objects\Endpoint;
use App\Services\Databases\Hosts\HostCreationService;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\CreateRecord;
use Closure;
use Exception;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use PDOException;
class CreateDatabaseHost extends CreateRecord
{
private HostCreationService $service;
protected static string $resource = DatabaseHostResource::class;
protected ?string $heading = 'Database Hosts';
@@ -24,6 +29,11 @@ class CreateDatabaseHost extends CreateRecord
protected ?string $subheading = '(database servers that can have individual databases)';
public function boot(HostCreationService $service): void
{
$this->service = $service;
}
public function form(Form $form): Form
{
return $form
@@ -50,7 +60,7 @@ class CreateDatabaseHost extends CreateRecord
->numeric()
->default(3306)
->minValue(0)
->maxValue(65535),
->maxValue(Endpoint::PORT_CEIL),
TextInput::make('max_databases')
->label('Max databases')
->helpertext('Blank is unlimited.')
@@ -94,10 +104,10 @@ class CreateDatabaseHost extends CreateRecord
protected function handleRecordCreation(array $data): Model
{
return resolve(HostCreationService::class)->handle($data);
return $this->service->handle($data);
}
public function exception($e, $stopPropagation): void
public function exception(Exception $e, Closure $stopPropagation): void
{
if ($e instanceof PDOException) {
Notification::make()

View File

@@ -5,15 +5,18 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
use App\Models\DatabaseHost;
use App\Models\Objects\Endpoint;
use App\Services\Databases\Hosts\HostUpdateService;
use Closure;
use Exception;
use Filament\Actions;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
use PDOException;
@@ -21,6 +24,13 @@ class EditDatabaseHost extends EditRecord
{
protected static string $resource = DatabaseHostResource::class;
private HostUpdateService $hostUpdateService;
public function boot(HostUpdateService $hostUpdateService): void
{
$this->hostUpdateService = $hostUpdateService;
}
public function form(Form $form): Form
{
return $form
@@ -46,7 +56,7 @@ class EditDatabaseHost extends EditRecord
->required()
->numeric()
->minValue(0)
->maxValue(65535),
->maxValue(Endpoint::PORT_CEIL),
TextInput::make('max_databases')
->label('Max databases')
->helpertext('Blank is unlimited.')
@@ -97,12 +107,16 @@ class EditDatabaseHost extends EditRecord
];
}
protected function handleRecordUpdate($record, array $data): Model
protected function handleRecordUpdate(Model $record, array $data): Model
{
return resolve(HostUpdateService::class)->handle($record->id, $data);
if (!$record instanceof DatabaseHost) {
return $record;
}
return $this->hostUpdateService->handle($record, $data);
}
public function exception($e, $stopPropagation): void
public function exception(Exception $e, Closure $stopPropagation): void
{
if ($e instanceof PDOException) {
Notification::make()

View File

@@ -3,9 +3,11 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Models\DatabaseHost;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
@@ -30,13 +32,16 @@ class ListDatabaseHosts extends ListRecords
->sortable(),
TextColumn::make('username')
->searchable(),
TextColumn::make('max_databases')
->numeric()
->sortable(),
TextColumn::make('databases_count')
->counts('databases')
->icon('tabler-database')
->label('Databases'),
TextColumn::make('node.name')
->numeric()
->icon('tabler-server-2')
->placeholder('No Nodes')
->sortable(),
])
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
->actions([
EditAction::make(),
])
@@ -45,13 +50,23 @@ class ListDatabaseHosts extends ListRecords
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete databasehost')),
]),
])
->emptyStateIcon('tabler-database')
->emptyStateDescription('')
->emptyStateHeading('No Database Hosts')
->emptyStateActions([
CreateAction::make('create')
->label('Create Database Host')
->button(),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make('create')->label('New Database Host'),
Actions\CreateAction::make('create')
->label('Create Database Host')
->hidden(fn () => DatabaseHost::count() <= 0),
];
}
}

View File

@@ -8,6 +8,7 @@ use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
@@ -40,6 +41,7 @@ class DatabasesRelationManager extends RelationManager
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
]);
}
public function table(Table $table): Table
{
return $table
@@ -60,7 +62,7 @@ class DatabasesRelationManager extends RelationManager
]);
}
protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void
protected function rotatePassword(DatabasePasswordService $service, Database $database, Set $set, Get $get): void
{
$newPassword = $service->handle($database);
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database');

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
@@ -20,13 +21,6 @@ class DatabaseResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -21,9 +21,12 @@ class CreateDatabase extends CreateRecord
->searchable()
->preload()
->required(),
TextInput::make('database_host_id')
->required()
->numeric(),
Select::make('database_host_id')
->relationship('host', 'name')
->searchable()
->selectablePlaceholder(false)
->preload()
->required(),
TextInput::make('database')
->required()
->maxLength(255),

View File

@@ -21,13 +21,6 @@ class EggResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getGloballySearchableAttributes(): array
{
return ['name', 'tags', 'uuid', 'id'];

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Resources\EggResource;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
@@ -15,10 +16,9 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Pages\CreateRecord;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@@ -27,6 +27,7 @@ class CreateEgg extends CreateRecord
protected static string $resource = EggResource::class;
protected static bool $canCreateAnother = false;
public function form(Form $form): Form
{
return $form
@@ -64,7 +65,7 @@ class CreateEgg extends CreateRecord
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Toggle::make('force_outgoing_ip')
->hintIcon('tabler-question-mark')
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary endpoint.
Required for certain games to work properly when the Node has multiple public IP addresses.
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
Hidden::make('script_is_privileged')
@@ -155,7 +156,7 @@ class CreateEgg extends CreateRecord
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
)
->required(),
Textarea::make('description')->columnSpanFull(),

View File

@@ -10,7 +10,6 @@ use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\FileUpload;
@@ -26,6 +25,7 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
@@ -40,6 +40,7 @@ class EditEgg extends EditRecord
Tabs::make()->tabs([
Tab::make('Configuration')
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->icon('tabler-egg')
->schema([
TextInput::make('name')
->required()
@@ -80,8 +81,9 @@ class EditEgg extends EditRecord
->helperText('')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Toggle::make('force_outgoing_ip')
->inline(false)
->hintIcon('tabler-question-mark')
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's endpoint.
Required for certain games to work properly when the Node has multiple public IP addresses.
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
Hidden::make('script_is_privileged')
@@ -105,9 +107,9 @@ class EditEgg extends EditRecord
->valueLabel('Image URI')
->helperText('The docker images available to servers using this egg.'),
]),
Tab::make('Process Management')
->columns()
->icon('tabler-server-cog')
->schema([
Select::make('config_from')
->label('Copy Settings From')
@@ -130,6 +132,7 @@ class EditEgg extends EditRecord
]),
Tab::make('Egg Variables')
->columnSpanFull()
->icon('tabler-variable')
->schema([
Repeater::make('variables')
->label('')
@@ -165,7 +168,7 @@ class EditEgg extends EditRecord
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
)
->required(),
Textarea::make('description')->columnSpanFull(),
@@ -211,22 +214,19 @@ class EditEgg extends EditRecord
]),
Tab::make('Install Script')
->columns(3)
->icon('tabler-file-download')
->schema([
Select::make('copy_script_from')
->placeholder('None')
->relationship('scriptFrom', 'name', ignoreRecord: true),
TextInput::make('script_container')
->required()
->maxLength(255)
->default('alpine:3.4'),
TextInput::make('script_entry')
->required()
->maxLength(255)
->default('ash'),
MonacoEditor::make('script_install')
->label('Install Script')
->columnSpanFull()
@@ -234,7 +234,6 @@ class EditEgg extends EditRecord
->language('shell')
->view('filament.plugins.monaco-editor'),
]),
])->columnSpanFull()->persistTabInQueryString(),
]);
}
@@ -281,10 +280,7 @@ class EditEgg extends EditRecord
->contained(false),
])
->action(function (array $data, Egg $egg): void {
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
->action(function (array $data, Egg $egg, EggImporterService $eggImportService): void {
if (!empty($data['egg'])) {
try {
$eggImportService->fromFile($data['egg'], $egg);

View File

@@ -14,7 +14,7 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
@@ -49,7 +49,7 @@ class ListEggs extends ListRecords
])
->actions([
EditAction::make(),
Tables\Actions\Action::make('export')
Action::make('export')
->icon('tabler-download')
->label('Export')
->color('primary')
@@ -57,6 +57,39 @@ class ListEggs extends ListRecords
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json'))
->authorize(fn () => auth()->user()->can('export egg')),
Action::make('update')
->icon('tabler-cloud-download')
->label('Update')
->color('success')
->requiresConfirmation()
->modalHeading('Are you sure you want to update this egg?')
->modalDescription('If you made any changes to the egg they will be overwritten!')
->modalIconColor('danger')
->modalSubmitAction(fn (Actions\StaticAction $action) => $action->color('danger'))
->action(function (Egg $egg, EggImporterService $eggImporterService) {
try {
$eggImporterService->fromUrl($egg->update_url, $egg);
cache()->forget("eggs.{$egg->uuid}.update");
} catch (Exception $exception) {
Notification::make()
->title('Egg Update failed')
->body($exception->getMessage())
->danger()
->send();
report($exception);
return;
}
Notification::make()
->title('Egg updated')
->success()
->send();
})
->authorize(fn () => auth()->user()->can('import egg'))
->visible(fn (Egg $egg) => cache()->get("eggs.{$egg->uuid}.update", false)),
])
->bulkActions([
BulkActionGroup::make([
@@ -65,6 +98,7 @@ class ListEggs extends ListRecords
]),
]);
}
protected function getHeaderActions(): array
{
return [
@@ -97,10 +131,7 @@ class ListEggs extends ListRecords
->contained(false),
])
->action(function (array $data): void {
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
->action(function (array $data, EggImporterService $eggImportService): void {
if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg'];

View File

@@ -4,7 +4,6 @@ namespace App\Filament\Resources\EggResource\RelationManagers;
use App\Models\Server;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -16,7 +15,7 @@ class ServersRelationManager extends RelationManager
{
return $table
->recordTitleAttribute('servers')
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned to this Egg.')
->searchable(false)
->columns([
TextColumn::make('user.username')
@@ -33,11 +32,6 @@ class ServersRelationManager extends RelationManager
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
TextColumn::make('image')
->label('Docker Image'),
SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
]);
}
}

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
@@ -18,13 +19,6 @@ class MountResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -4,14 +4,14 @@ namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use Filament\Actions;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\EditRecord;
class EditMount extends EditRecord
{
@@ -96,6 +96,7 @@ class EditMount extends EditRecord
'lg' => 2,
]);
}
protected function getHeaderActions(): array
{
return [

View File

@@ -17,6 +17,7 @@ use Filament\Tables\Table;
class ListMounts extends ListRecords
{
protected static string $resource = MountResource::class;
public function table(Table $table): Table
{
return $table
@@ -56,6 +57,7 @@ class ListMounts extends ListRecords
->button(),
]);
}
protected function getHeaderActions(): array
{
return [

View File

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

View File

@@ -3,8 +3,9 @@
namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use Filament\Forms\Components\Actions\Action;
use App\Models\Objects\Endpoint;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
@@ -139,7 +140,7 @@ class CreateNode extends CreateRecord
->label(trans('strings.port'))
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
->minValue(1)
->maxValue(65535)
->maxValue(Endpoint::PORT_CEIL)
->default(8080)
->required()
->integer(),
@@ -153,7 +154,6 @@ class CreateNode extends CreateRecord
'lg' => 2,
])
->required()
->regex('/[a-zA-Z0-9_\.\- ]+/')
->helperText('This name is for display only and can be changed later.')
->maxLength(100),
@@ -220,7 +220,7 @@ class CreateNode extends CreateRecord
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label('Automatic Allocation')->inline()
->label('Use Node for deployment?')->inline()
->options([
true => 'Yes',
false => 'No',
@@ -230,11 +230,7 @@ class CreateNode extends CreateRecord
false => 'danger',
]),
TagsInput::make('tags')
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented')
->placeholder('Add Tags')
->columnSpan(2),
TextInput::make('upload_size')
->label('Upload Limit')
@@ -249,7 +245,7 @@ class CreateNode extends CreateRecord
->columnSpan(1)
->label('SFTP Port')
->minValue(1)
->maxValue(65535)
->maxValue(Endpoint::PORT_CEIL)
->default(2022)
->required()
->integer(),
@@ -403,7 +399,7 @@ class CreateNode extends CreateRecord
protected function getRedirectUrlParameters(): array
{
return [
'tab' => '-configuration-tab',
'tab' => '-configuration-file-tab',
];
}

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Models\Node;
use App\Models\Objects\Endpoint;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
use Filament\Forms;
@@ -83,7 +84,7 @@ class EditNode extends EditRecord
if (request()->isSecure()) {
return '
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
You must use a domain name, because you cannot get SSL certificates for IP Addresses
You must use a domain name, because you cannot get SSL certificates for IP Addresses.
';
}
@@ -98,7 +99,7 @@ class EditNode extends EditRecord
->hintColor('danger')
->hint(function ($state) {
if (is_ip($state) && request()->isSecure()) {
return 'You cannot connect to an IP Address over SSL';
return 'You cannot connect to an IP Address over SSL!';
}
return '';
@@ -130,11 +131,9 @@ class EditNode extends EditRecord
$set('dns', false);
})
->maxLength(255),
TextInput::make('ip')
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label('DNS Record Check')
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
@@ -157,7 +156,6 @@ class EditNode extends EditRecord
'md' => 1,
'lg' => 1,
]),
TextInput::make('daemon_listen')
->columnSpan([
'default' => 1,
@@ -168,11 +166,10 @@ class EditNode extends EditRecord
->label(trans('strings.port'))
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
->minValue(1)
->maxValue(65535)
->maxValue(Endpoint::PORT_CEIL)
->default(8080)
->required()
->integer(),
TextInput::make('name')
->label('Display Name')
->columnSpan([
@@ -182,10 +179,8 @@ class EditNode extends EditRecord
'lg' => 2,
])
->required()
->regex('/[a-zA-Z0-9_\.\- ]+/')
->helperText('This name is for display only and can be changed later.')
->maxLength(100),
ToggleButtons::make('scheme')
->label('Communicate over SSL')
->columnSpan([
@@ -235,11 +230,7 @@ class EditNode extends EditRecord
->disabled(),
TagsInput::make('tags')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented'),
->placeholder('Add Tags'),
TextInput::make('upload_size')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->label('Upload Limit')
@@ -253,7 +244,7 @@ class EditNode extends EditRecord
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('SFTP Port')
->minValue(1)
->maxValue(65535)
->maxValue(Endpoint::PORT_CEIL)
->default(2022)
->required()
->integer(),
@@ -263,7 +254,7 @@ class EditNode extends EditRecord
->helperText('Display alias for the SFTP address. Leave empty to use the Node FQDN.'),
ToggleButtons::make('public')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
->label('Automatic Allocation')->inline()
->label('Use Node for deployment?')->inline()
->options([
true => 'Yes',
false => 'No',
@@ -446,6 +437,16 @@ class EditNode extends EditRecord
$data['config'] = $node->getYamlConfiguration();
if (!is_ip($node->fqdn)) {
$validRecords = gethostbynamel($node->fqdn);
if ($validRecords) {
$data['dns'] = true;
$data['ip'] = collect($validRecords)->first();
} else {
$data['dns'] = false;
}
}
return $data;
}
@@ -453,6 +454,7 @@ class EditNode extends EditRecord
{
return [];
}
protected function getHeaderActions(): array
{
return [
@@ -468,11 +470,12 @@ class EditNode extends EditRecord
$this->fillForm();
}
protected function getColumnSpan()
protected function getColumnSpan(): ?int
{
return null;
}
protected function getColumnStart()
protected function getColumnStart(): ?int
{
return null;
}

View File

@@ -13,6 +13,7 @@ use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Number;
class ListNodes extends ListRecords
{
@@ -47,18 +48,18 @@ class ListNodes extends ListRecords
->icon('tabler-device-desktop-analytics')
->numeric()
->suffix(config('panel.use_binary_prefix') ? ' GiB' : ' GB')
->formatStateUsing(fn ($state) => number_format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), 2))
->formatStateUsing(fn ($state) => Number::format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), maxPrecision: 2, locale: auth()->user()->language))
->sortable(),
TextColumn::make('disk')
->visibleFrom('sm')
->icon('tabler-file')
->numeric()
->suffix(config('panel.use_binary_prefix') ? ' GiB' : ' GB')
->formatStateUsing(fn ($state) => number_format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), 2))
->formatStateUsing(fn ($state) => Number::format($state / (config('panel.use_binary_prefix') ? 1024 : 1000), maxPrecision: 2, locale: auth()->user()->language))
->sortable(),
TextColumn::make('cpu')
->visibleFrom('sm')
->icon('tabler-file')
->icon('tabler-cpu')
->numeric()
->suffix(' %')
->sortable(),

View File

@@ -1,160 +0,0 @@
<?php
namespace App\Filament\Resources\NodeResource\RelationManagers;
use App\Models\Allocation;
use App\Models\Node;
use App\Services\Allocations\AssignmentService;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;
/**
* @method Node getOwnerRecord()
*/
class AllocationsRelationManager extends RelationManager
{
protected static string $relationship = 'allocations';
protected static ?string $icon = 'tabler-plug-connected';
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('ip')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('ip')
// Non Primary Allocations
// ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
// All assigned allocations
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->searchable()
->columns([
TextColumn::make('id'),
TextColumn::make('port')
->searchable()
->label('Port'),
TextColumn::make('server.name')
->label('Server')
->icon('tabler-brand-docker')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
TextInputColumn::make('ip_alias')
->searchable()
->label('Alias'),
TextInputColumn::make('ip')
->searchable()
->label('IP'),
])
->filters([
//
])
->actions([
//
])
->headerActions([
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
->form(fn () => [
TextInput::make('allocation_ip')
->datalist($this->getOwnerRecord()->ipAddresses())
->label('IP Address')
->inlineLabel()
->ipv4()
->helperText("Usually your machine's public IP unless you are port forwarding.")
->required(),
TextInput::make('allocation_alias')
->label('Alias')
->inlineLabel()
->default(null)
->helperText('Optional display name to help you remember what these are.')
->required(false),
TagsInput::make('allocation_ports')
->placeholder('Examples: 27015, 27017-27019')
->helperText(new HtmlString('
These are the ports that users can connect to this Server through.
<br />
You would have to port forward these on your home network.
'))
->label('Ports')
->inlineLabel()
->live()
->afterStateUpdated(function ($state, Set $set) {
$ports = collect();
$update = false;
foreach ($state as $portEntry) {
if (!str_contains($portEntry, '-')) {
if (is_numeric($portEntry)) {
$ports->push((int) $portEntry);
continue;
}
// Do not add non numerical ports
$update = true;
continue;
}
$update = true;
[$start, $end] = explode('-', $portEntry);
if (!is_numeric($start) || !is_numeric($end)) {
continue;
}
$start = max((int) $start, 0);
$end = min((int) $end, 2 ** 16 - 1);
foreach (range($start, $end) as $i) {
$ports->push($i);
}
}
$uniquePorts = $ports->unique()->values();
if ($ports->count() > $uniquePorts->count()) {
$update = true;
$ports = $uniquePorts;
}
$sortedPorts = $ports->sort()->values();
if ($sortedPorts->all() !== $ports->all()) {
$update = true;
$ports = $sortedPorts;
}
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
if ($update) {
$set('allocation_ports', $ports->all());
}
})
->splitKeys(['Tab', ' ', ','])
->required(),
])
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord(), $data)),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete allocation')),
]),
]);
}
}

View File

@@ -3,10 +3,9 @@
namespace App\Filament\Resources\NodeResource\RelationManagers;
use App\Models\Server;
use Filament\Tables\Columns\SelectColumn;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Resources\RelationManagers\RelationManager;
class NodesRelationManager extends RelationManager
{
@@ -33,11 +32,6 @@ class NodesRelationManager extends RelationManager
->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user]))
->sortable(),
SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
TextColumn::make('cpu')->icon('tabler-cpu'),
TextColumn::make('databases_count')

View File

@@ -7,10 +7,12 @@ use Carbon\Carbon;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Number;
class NodeCpuChart extends ChartWidget
{
protected static ?string $pollingInterval = '5s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;
@@ -24,7 +26,7 @@ class NodeCpuChart extends ChartWidget
$cpu = collect(cache()->get("nodes.$node->id.cpu_percent"))
->slice(-10)
->map(fn ($value, $key) => [
'cpu' => number_format($value * $threads, 2),
'cpu' => Number::format($value * $threads, maxPrecision: 2, locale: auth()->user()->language),
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
])
->all();
@@ -73,8 +75,8 @@ class NodeCpuChart extends ChartWidget
$node = $this->record;
$threads = $node->systemInformation()['cpu_count'] ?? 0;
$cpu = number_format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, 2);
$max = number_format($threads * 100) . '%';
$cpu = Number::format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, maxPrecision: 2, locale: auth()->user()->language);
$max = Number::format($threads * 100, locale: auth()->user()->language) . '%';
return 'CPU - ' . $cpu . '% Of ' . $max;
}

View File

@@ -7,10 +7,12 @@ use Carbon\Carbon;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Number;
class NodeMemoryChart extends ChartWidget
{
protected static ?string $pollingInterval = '5s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;
@@ -22,7 +24,7 @@ class NodeMemoryChart extends ChartWidget
$memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10)
->map(fn ($value, $key) => [
'memory' => config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000,
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language),
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
])
->all();
@@ -73,12 +75,12 @@ class NodeMemoryChart extends ChartWidget
$totalMemory = collect(cache()->get("nodes.$node->id.memory_total"))->last();
$used = config('panel.use_binary_prefix')
? number_format($latestMemoryUsed / 1024 / 1024 / 1024, 2) .' GiB'
: number_format($latestMemoryUsed / 1000 / 1000 / 1000, 2) . ' GB';
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
$total = config('panel.use_binary_prefix')
? number_format($totalMemory / 1024 / 1024 / 1024, 2) .' GiB'
: number_format($totalMemory / 1000 / 1000 / 1000, 2) . ' GB';
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
return 'Memory - ' . $used . ' Of ' . $total;
}

View File

@@ -9,7 +9,9 @@ use Illuminate\Database\Eloquent\Model;
class NodeStorageChart extends ChartWidget
{
protected static ?string $heading = 'Storage';
protected static ?string $pollingInterval = '60s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;

View File

@@ -19,13 +19,6 @@ class ServerResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -3,39 +3,69 @@
namespace App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\Node;
use App\Models\Objects\Endpoint;
use App\Models\User;
use App\Services\Allocations\AssignmentService;
use App\Services\Servers\RandomWordService;
use App\Services\Servers\ServerCreationService;
use App\Services\Users\UserCreationService;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Closure;
use Exception;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
use Closure;
use LogicException;
class CreateServer extends CreateRecord
{
protected static string $resource = ServerResource::class;
protected static bool $canCreateAnother = false;
public ?Node $node = null;
public ?Egg $egg = null;
public array $ports = [];
public array $eggDefaultPorts = [];
private ServerCreationService $serverCreationService;
public function boot(ServerCreationService $serverCreationService): void
{
$this->serverCreationService = $serverCreationService;
}
public function form(Form $form): Form
{
return $form
->schema([
Wizard::make([
Wizard\Step::make('Information')
Step::make('Information')
->label('Information')
->icon('tabler-info-circle')
->completedIcon('tabler-check')
@@ -46,12 +76,12 @@ class CreateServer extends CreateRecord
'lg' => 6,
])
->schema([
Forms\Components\TextInput::make('name')
TextInput::make('name')
->prefixIcon('tabler-server')
->label('Name')
->suffixAction(Forms\Components\Actions\Action::make('random')
->icon('tabler-dice-' . random_int(1, 6))
->action(function (Forms\Set $set, Forms\Get $get) {
->action(function (Set $set, Get $get) {
$egg = Egg::find($get('egg_id'));
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
@@ -68,7 +98,7 @@ class CreateServer extends CreateRecord
->required()
->maxLength(255),
Forms\Components\Select::make('owner_id')
Select::make('owner_id')
->preload()
->prefixIcon('tabler-user')
->default(auth()->user()->id)
@@ -83,29 +113,30 @@ class CreateServer extends CreateRecord
->searchable(['username', 'email'])
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->isRootAdmin() ? '(admin)' : ''))
->createOptionForm([
Forms\Components\TextInput::make('username')
TextInput::make('username')
->alphaNum()
->required()
->maxLength(255),
Forms\Components\TextInput::make('email')
TextInput::make('email')
->email()
->required()
->unique()
->maxLength(255),
Forms\Components\TextInput::make('password')
TextInput::make('password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
->password(),
])
->createOptionUsing(function ($data) {
resolve(UserCreationService::class)->handle($data);
->createOptionUsing(function ($data, UserCreationService $service) {
$service->handle($data);
$this->refreshForm();
})
->required(),
Forms\Components\Select::make('node_id')
Select::make('node_id')
->disabledOn('edit')
->prefixIcon('tabler-server-2')
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
@@ -120,175 +151,11 @@ class CreateServer extends CreateRecord
->searchable()
->preload()
->afterStateUpdated(function (Forms\Set $set, $state) {
$set('allocation_id', null);
$this->node = Node::find($state);
})
->required(),
Forms\Components\Select::make('allocation_id')
->preload()
->live()
->prefixIcon('tabler-network')
->label('Primary Allocation')
->columnSpan([
'default' => 2,
'sm' => 3,
'md' => 2,
'lg' => 3,
])
->disabled(fn (Forms\Get $get) => $get('node_id') === null)
->searchable(['ip', 'port', 'ip_alias'])
->afterStateUpdated(function (Forms\Set $set) {
$set('allocation_additional', null);
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
})
->getOptionLabelFromRecordUsing(
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
)
->placeholder(function (Forms\Get $get) {
$node = Node::find($get('node_id'));
if ($node?->allocations) {
return 'Select an Allocation';
}
return 'Create a New Allocation';
})
->relationship(
'allocation',
'ip',
fn (Builder $query, Forms\Get $get) => $query
->where('node_id', $get('node_id'))
->whereNull('server_id'),
)
->createOptionForm(fn (Forms\Get $get) => [
Forms\Components\TextInput::make('allocation_ip')
->datalist(Node::find($get('node_id'))?->ipAddresses() ?? [])
->label('IP Address')
->inlineLabel()
->ipv4()
->helperText("Usually your machine's public IP unless you are port forwarding.")
// ->selectablePlaceholder(false)
->required(),
Forms\Components\TextInput::make('allocation_alias')
->label('Alias')
->inlineLabel()
->default(null)
->datalist([
$get('name'),
Egg::find($get('egg_id'))?->name,
])
->helperText('Optional display name to help you remember what these are.')
->required(false),
Forms\Components\TagsInput::make('allocation_ports')
->placeholder('Examples: 27015, 27017-27019')
->helperText(new HtmlString('
These are the ports that users can connect to this Server through.
<br />
You would have to port forward these on your home network.
'))
->label('Ports')
->inlineLabel()
->live()
->afterStateUpdated(function ($state, Forms\Set $set) {
$ports = collect();
$update = false;
foreach ($state as $portEntry) {
if (!str_contains($portEntry, '-')) {
if (is_numeric($portEntry)) {
$ports->push((int) $portEntry);
continue;
}
// Do not add non-numerical ports
$update = true;
continue;
}
$update = true;
[$start, $end] = explode('-', $portEntry);
if (!is_numeric($start) || !is_numeric($end)) {
continue;
}
$start = max((int) $start, 0);
$end = min((int) $end, 2 ** 16 - 1);
$range = $start <= $end ? range($start, $end) : range($end, $start);
foreach ($range as $i) {
if ($i > 1024 && $i <= 65535) {
$ports->push($i);
}
}
}
$uniquePorts = $ports->unique()->values();
if ($ports->count() > $uniquePorts->count()) {
$update = true;
$ports = $uniquePorts;
}
$sortedPorts = $ports->sort()->values();
if ($sortedPorts->all() !== $ports->all()) {
$update = true;
$ports = $sortedPorts;
}
if ($update) {
$set('allocation_ports', $ports->all());
}
})
->splitKeys(['Tab', ' ', ','])
->required(),
])
->createOptionUsing(function (array $data, Forms\Get $get): int {
return collect(
resolve(AssignmentService::class)->handle(Node::find($get('node_id')), $data)
)->first();
})
->required(),
Forms\Components\Repeater::make('allocation_additional')
->label('Additional Allocations')
->columnSpan([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 3,
])
->addActionLabel('Add Allocation')
->disabled(fn (Forms\Get $get) => $get('allocation_id') === null)
// ->addable() TODO disable when all allocations are taken
// ->addable() TODO disable until first additional allocation is selected
->simple(
Forms\Components\Select::make('extra_allocations')
->live()
->preload()
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->prefixIcon('tabler-network')
->label('Additional Allocations')
->columnSpan(2)
->disabled(fn (Forms\Get $get) => $get('../../node_id') === null)
->searchable(['ip', 'port', 'ip_alias'])
->getOptionLabelFromRecordUsing(
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
)
->placeholder('Select additional Allocations')
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->relationship(
'allocations',
'ip',
fn (Builder $query, Forms\Get $get, Forms\Components\Select $component, $state) => $query
->where('node_id', $get('../../node_id'))
->whereNot('id', $get('../../allocation_id'))
->whereNull('server_id'),
),
),
Forms\Components\Textarea::make('description')
Textarea::make('description')
->placeholder('Description')
->rows(3)
->columnSpan([
@@ -297,10 +164,10 @@ class CreateServer extends CreateRecord
'md' => 6,
'lg' => 6,
])
->label('Notes'),
->label('Description'),
]),
Wizard\Step::make('Egg Configuration')
Step::make('Egg Configuration')
->label('Egg Configuration')
->icon('tabler-egg')
->completedIcon('tabler-check')
@@ -311,47 +178,33 @@ class CreateServer extends CreateRecord
'lg' => 6,
])
->schema([
Forms\Components\Select::make('egg_id')
Select::make('egg_id')
->prefixIcon('tabler-egg')
->relationship('egg', 'name')
->columnSpan([
'default' => 1,
'default' => 2,
'sm' => 2,
'md' => 2,
'lg' => 4,
])
->relationship('egg', 'name')
->searchable()
->preload()
->live()
->afterStateUpdated(function ($state, Forms\Set $set, Forms\Get $get, $old) {
$egg = Egg::query()->find($state);
$set('startup', $egg->startup ?? '');
->afterStateUpdated(function ($state, Set $set, Get $get, $old) {
$this->egg = Egg::query()->find($state);
$set('startup', $this->egg?->startup);
$set('image', '');
$variables = $egg->variables ?? [];
$serverVariables = collect();
foreach ($variables as $variable) {
$serverVariables->add($variable->toArray());
}
$variables = [];
$set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all());
for ($i = 0; $i < $serverVariables->count(); $i++) {
$set("$path.$i.variable_value", $serverVariables[$i]['default_value']);
$set("$path.$i.variable_id", $serverVariables[$i]['id']);
$variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value'];
}
$set('environment', $variables);
$this->resetEggVariables($set, $get);
$previousEgg = Egg::query()->find($old);
if (!$get('name') || $previousEgg?->getKebabName() === $get('name')) {
$set('name', $egg->getKebabName());
$set('name', $this->egg->getKebabName());
}
})
->required(),
Forms\Components\ToggleButtons::make('skip_scripts')
ToggleButtons::make('skip_scripts')
->label('Run Egg Install Script?')
->default(false)
->columnSpan([
@@ -375,7 +228,7 @@ class CreateServer extends CreateRecord
->inline()
->required(),
Forms\Components\ToggleButtons::make('start_on_completion')
ToggleButtons::make('start_on_completion')
->label('Start Server After Install?')
->default(true)
->required()
@@ -399,16 +252,24 @@ class CreateServer extends CreateRecord
])
->inline(),
Forms\Components\Textarea::make('startup')
Textarea::make('startup')
->hintIcon('tabler-code')
->label('Startup Command')
->hidden(fn (Forms\Get $get) => $get('egg_id') === null)
->hidden(fn () => !$this->egg)
->required()
->live()
->disabled(fn (Forms\Get $get) => $this->egg === null)
->afterStateUpdated($this->resetEggVariables(...))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 4,
])
->rows(function ($state) {
return str($state)->explode("\n")->reduce(
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
1
0
);
})
->columnSpan([
@@ -418,24 +279,24 @@ class CreateServer extends CreateRecord
'lg' => 6,
]),
Forms\Components\Hidden::make('environment')->default([]),
Hidden::make('environment')->default([]),
Forms\Components\Section::make('Variables')
Section::make('Variables')
->icon('tabler-eggs')
->iconColor('primary')
->hidden(fn (Forms\Get $get) => $get('egg_id') === null)
->hidden(fn (Get $get) => $get('egg_id') === null)
->collapsible()
->columnSpanFull()
->schema([
Forms\Components\Placeholder::make('Select an egg first to show its variables!')
->hidden(fn (Forms\Get $get) => $get('egg_id')),
Placeholder::make('Select an egg first to show its variables!')
->hidden(fn (Get $get) => $get('egg_id')),
Forms\Components\Placeholder::make('The selected egg has no variables!')
->hidden(fn (Forms\Get $get) => !$get('egg_id') ||
Placeholder::make('The selected egg has no variables!')
->hidden(fn (Get $get) => !$get('egg_id') ||
Egg::query()->find($get('egg_id'))?->variables()?->count()
),
Forms\Components\Repeater::make('server_variables')
Repeater::make('server_variables')
->label('')
->relationship('serverVariables')
->saveRelationshipsBeforeChildrenUsing(null)
@@ -448,11 +309,11 @@ class CreateServer extends CreateRecord
->hidden(fn ($state) => empty($state))
->schema(function () {
$text = Forms\Components\TextInput::make('variable_value')
$text = TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->required(fn (Forms\Get $get) => in_array('required', $get('rules')))
->required(fn (Get $get) => in_array('required', $get('rules')))
->rules(
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $get('rules'),
]);
@@ -465,7 +326,7 @@ class CreateServer extends CreateRecord
},
);
$select = Forms\Components\Select::make('variable_value')
$select = Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
@@ -476,11 +337,11 @@ class CreateServer extends CreateRecord
$component = $component
->live(onBlur: true)
->hintIcon('tabler-code')
->label(fn (Forms\Get $get) => $get('name'))
->hintIconTooltip(fn (Forms\Get $get) => implode('|', $get('rules')))
->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}')
->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description'))
->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
->label(fn (Get $get) => $get('name'))
->hintIconTooltip(fn (Get $get) => implode('|', $get('rules')))
->prefix(fn (Get $get) => '{{' . $get('env_variable') . '}}')
->helperText(fn (Get $get) => empty($get('description')) ? '—' : $get('description'))
->afterStateUpdated(function (Set $set, Get $get, $state) {
$environment = $get($envPath = '../../environment');
$environment[$get('env_variable')] = $state;
$set($envPath, $environment);
@@ -492,12 +353,76 @@ class CreateServer extends CreateRecord
->columnSpan(2),
]),
]),
Wizard\Step::make('Environment Configuration')
Wizard\Step::make('Allocation')
->label('Allocation')
->icon('tabler-transfer-in')
->completedIcon('tabler-check')
->columns(4)
->schema([
Forms\Components\TagsInput::make('ports')
->columnSpan(2)
->hintIcon('tabler-question-mark')
->hintIconTooltip('Ports are limited from 1025 to 65535')
->placeholder('Example: 25565, 8080, 1337-1340')
->splitKeys(['Tab', ' ', ','])
->helperText(new HtmlString('
These are the ports that users can connect to this Server through.
You would typically port forward these on your home network.
'))
->label('Ports')
->afterStateUpdated(self::ports(...))
->live(),
Forms\Components\Repeater::make('assignments')
->columnSpan(2)
->defaultItems(fn () => count($this->eggDefaultPorts))
->label('Port Assignments')
->helperText(function (Forms\Get $get) {
if (empty($this->eggDefaultPorts)) {
return "This egg doesn't have any ports defined.";
}
if (empty($get('ports'))) {
return 'You must add ports to assign them!';
}
return '';
})
->live()
->addable(false)
->deletable(false)
->reorderable(false)
->simple(
Forms\Components\Select::make('port')
->live()
->placeholder('Select a Port')
->disabled(fn (Forms\Get $get) => empty($get('../../ports')) || empty($get('../../assignments')))
->prefix(function (Forms\Components\Component $component) {
$key = str($component->getStatePath())->beforeLast('.')->afterLast('.')->toString();
return $key;
})
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->options(fn (Forms\Get $get) => $this->ports)
->required(),
),
Forms\Components\Select::make('ip')
->label('IP Address')
->options(fn () => collect($this->node?->ipAddresses())->mapWithKeys(fn ($ip) => [$ip => $ip]))
->placeholder('Any')
->columnSpan(1),
]),
Step::make('Environment Configuration')
->label('Environment Configuration')
->icon('tabler-brand-docker')
->completedIcon('tabler-check')
->schema([
Forms\Components\Fieldset::make('Resource Limits')
Fieldset::make('Resource Limits')
->columnSpan(6)
->columns([
'default' => 1,
@@ -506,14 +431,14 @@ class CreateServer extends CreateRecord
'lg' => 3,
])
->schema([
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->live()
->options([
true => 'Unlimited',
@@ -525,9 +450,9 @@ class CreateServer extends CreateRecord
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->default(0)
@@ -537,15 +462,15 @@ class CreateServer extends CreateRecord
->minValue(0),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
ToggleButtons::make('unlimited_disk')
->label('Disk Space')->inlineLabel()->inline()
->default(true)
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->options([
true => 'Unlimited',
false => 'Limited',
@@ -556,9 +481,9 @@ class CreateServer extends CreateRecord
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->default(0)
@@ -568,14 +493,14 @@ class CreateServer extends CreateRecord
->minValue(0),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->live()
->options([
true => 'Unlimited',
@@ -587,9 +512,9 @@ class CreateServer extends CreateRecord
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->default(0)
@@ -600,23 +525,23 @@ class CreateServer extends CreateRecord
->helperText('100% equals one CPU core.'),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('swap_support')
ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')
->inlineLabel()
->inline()
->columnSpan(2)
->default('disabled')
->afterStateUpdated(function ($state, Forms\Set $set) {
->afterStateUpdated(function ($state, Set $set) {
$value = match ($state) {
'unlimited' => -1,
'disabled' => 0,
'limited' => 128,
default => throw new \LogicException('Invalid state'),
default => throw new LogicException('Invalid state'),
};
$set('swap', $value);
@@ -632,9 +557,9 @@ class CreateServer extends CreateRecord
'disabled' => 'danger',
]),
Forms\Components\TextInput::make('swap')
TextInput::make('swap')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
->hidden(fn (Get $get) => match ($get('swap_support')) {
'disabled', 'unlimited' => true,
default => false,
})
@@ -648,16 +573,16 @@ class CreateServer extends CreateRecord
->integer(),
]),
Forms\Components\Hidden::make('io')
Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion')
->default(500),
->default(config('panel.default_io_weight')),
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_killer')
ToggleButtons::make('oom_killer')
->label('OOM Killer')
->inlineLabel()->inline()
->default(false)
@@ -671,12 +596,12 @@ class CreateServer extends CreateRecord
true => 'danger',
]),
Forms\Components\TextInput::make('oom_disabled_hidden')
TextInput::make('oom_disabled_hidden')
->hidden(),
]),
]),
Forms\Components\Fieldset::make('Feature Limits')
Fieldset::make('Feature Limits')
->inlineLabel()
->columnSpan(6)
->columns([
@@ -686,21 +611,21 @@ class CreateServer extends CreateRecord
'lg' => 3,
])
->schema([
Forms\Components\TextInput::make('allocation_limit')
TextInput::make('allocation_limit')
->label('Allocations')
->suffixIcon('tabler-network')
->required()
->numeric()
->minValue(0)
->default(0),
Forms\Components\TextInput::make('database_limit')
TextInput::make('database_limit')
->label('Databases')
->suffixIcon('tabler-database')
->required()
->numeric()
->minValue(0)
->default(0),
Forms\Components\TextInput::make('backup_limit')
TextInput::make('backup_limit')
->label('Backups')
->suffixIcon('tabler-copy-check')
->required()
@@ -708,7 +633,7 @@ class CreateServer extends CreateRecord
->minValue(0)
->default(0),
]),
Forms\Components\Fieldset::make('Docker Settings')
Fieldset::make('Docker Settings')
->columns([
'default' => 1,
'sm' => 2,
@@ -717,10 +642,10 @@ class CreateServer extends CreateRecord
])
->columnSpan(6)
->schema([
Forms\Components\Select::make('select_image')
Select::make('select_image')
->label('Image Name')
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
->options(function ($state, Forms\Get $get, Forms\Set $set) {
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
->options(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
@@ -741,10 +666,10 @@ class CreateServer extends CreateRecord
'lg' => 2,
]),
Forms\Components\TextInput::make('image')
TextInput::make('image')
->label('Image')
->debounce(500)
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
->afterStateUpdated(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
@@ -762,13 +687,13 @@ class CreateServer extends CreateRecord
'lg' => 2,
]),
Forms\Components\KeyValue::make('docker_labels')
KeyValue::make('docker_labels')
->label('Container Labels')
->keyLabel('Title')
->valueLabel('Description')
->columnSpanFull(),
Forms\Components\CheckboxList::make('mounts')
CheckboxList::make('mounts')
->live()
->relationship('mounts')
->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
@@ -804,32 +729,36 @@ class CreateServer extends CreateRecord
protected function handleRecordCreation(array $data): Model
{
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
$ipAddress = $data['ip'] ?? Endpoint::INADDR_ANY;
foreach ($data['ports'] ?? [] as $i => $port) {
$data['ports'][$i] = (string) new Endpoint($port, $ipAddress);
}
/** @var ServerCreationService $service */
$service = resolve(ServerCreationService::class);
foreach (array_keys($this->eggDefaultPorts) as $i => $env) {
$data['environment'][$env] = $data['ports'][$data['assignments'][$i]];
}
return $service->handle($data);
return $this->serverCreationService->handle($data, validateVariables: false);
}
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
private function shouldHideComponent(Get $get, Component $component): bool
{
$containsRuleIn = collect($get('rules'))->reduce(
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
);
if ($component instanceof Forms\Components\Select) {
if ($component instanceof Select) {
return $containsRuleIn;
}
if ($component instanceof Forms\Components\TextInput) {
if ($component instanceof TextInput) {
return !$containsRuleIn;
}
throw new \Exception('Component type not supported: ' . $component::class);
throw new Exception('Component type not supported: ' . $component::class);
}
private function getSelectOptionsFromRules(Forms\Get $get): array
private function getSelectOptionsFromRules(Get $get): array
{
$inRule = collect($get('rules'))->reduce(
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
@@ -842,4 +771,79 @@ class CreateServer extends CreateRecord
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
public function ports(array $state, Forms\Set $set): void
{
$ports = collect();
foreach ($state as $portEntry) {
if (str_contains($portEntry, '-')) {
[$start, $end] = explode('-', $portEntry);
if (!is_numeric($start) || !is_numeric($end)) {
continue;
}
$start = max((int) $start, Endpoint::PORT_FLOOR);
$end = min((int) $end, Endpoint::PORT_CEIL);
for ($i = $start; $i <= $end; $i++) {
$ports->push($i);
}
}
if (!is_numeric($portEntry)) {
continue;
}
$ports->push((int) $portEntry);
}
$uniquePorts = $ports->unique()->values();
if ($ports->count() > $uniquePorts->count()) {
$ports = $uniquePorts;
}
$ports = $ports->filter(fn ($port) => $port > Endpoint::PORT_FLOOR && $port < Endpoint::PORT_CEIL)->values();
$set('ports', $ports->all());
$this->ports = $ports->all();
}
public function resetEggVariables(Forms\Set $set, Forms\Get $get): void
{
$set('assignments', []);
$i = 0;
$this->eggDefaultPorts = [];
if (str_contains($get('startup'), '{{SERVER_PORT}}') || str_contains($this->egg->config_files, '{{server.allocations.default.port}}')) {
$this->eggDefaultPorts['SERVER_PORT'] = null;
$set('assignments.SERVER_PORT', ['port' => null]);
}
$variables = $this->egg->variables ?? [];
$serverVariables = collect();
$this->ports = [];
foreach ($variables as $variable) {
if (in_array('port', $variable->rules)) {
$this->eggDefaultPorts[$variable->env_variable] = $variable->default_value;
$this->ports[] = (int) $variable->default_value;
$set("assignments.$variable->env_variable", ['port' => $i++]);
continue;
}
$serverVariables->add($variable->toArray());
}
$set('ports', $this->ports);
$variables = [];
$set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all());
for ($i = 0; $i < $serverVariables->count(); $i++) {
$set("$path.$i.variable_value", $serverVariables[$i]['default_value']);
$set("$path.$i.variable_id", $serverVariables[$i]['id']);
$variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value'];
}
$set('environment', $variables);
}
}

View File

@@ -2,6 +2,11 @@
namespace App\Filament\Resources\ServerResource\Pages;
use App\Models\Node;
use App\Models\Objects\Endpoint;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\HtmlString;
use App\Models\Database;
use App\Services\Databases\DatabaseManagementService;
use App\Services\Databases\DatabasePasswordService;
@@ -23,17 +28,32 @@ use App\Models\Egg;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Services\Servers\ServerDeletionService;
use Closure;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Validator;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class EditServer extends EditRecord
{
public ?Node $node = null;
public ?Egg $egg = null;
public array $ports = [];
public array $eggDefaultPorts = [];
protected static string $resource = ServerResource::class;
public function form(Form $form): Form
@@ -50,15 +70,35 @@ class EditServer extends EditRecord
])
->columnSpanFull()
->tabs([
Tabs\Tab::make('Information')
Tab::make('Information')
->icon('tabler-info-circle')
->schema([
Forms\Components\TextInput::make('name')
Forms\Components\ToggleButtons::make('condition')
->label('Status')
->formatStateUsing(fn (Server $server) => $server->condition)
->options(fn ($state) => collect(array_merge(ContainerStatus::cases(), ServerState::cases()))
->filter(fn ($condition) => $condition->value === $state)
->mapWithKeys(fn ($state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()])
)
->colors(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys(
fn ($status) => [$status->value => $status->color()]
))
->icons(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys(
fn ($status) => [$status->value => $status->icon()]
))
->columnSpan([
'default' => 2,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
TextInput::make('name')
->prefixIcon('tabler-server')
->label('Display Name')
->suffixAction(Forms\Components\Actions\Action::make('random')
->suffixAction(Action::make('random')
->icon('tabler-dice-' . random_int(1, 6))
->action(function (Forms\Set $set, Forms\Get $get) {
->action(function (Set $set, Get $get) {
$egg = Egg::find($get('egg_id'));
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
@@ -75,7 +115,7 @@ class EditServer extends EditRecord
->required()
->maxLength(255),
Forms\Components\Select::make('owner_id')
Select::make('owner_id')
->prefixIcon('tabler-user')
->label('Owner')
->columnSpan([
@@ -89,7 +129,7 @@ class EditServer extends EditRecord
->preload()
->required(),
Forms\Components\ToggleButtons::make('condition')
ToggleButtons::make('condition')
->label('Server Status')
->formatStateUsing(fn (Server $server) => $server->condition)
->options(fn ($state) => collect(array_merge(ContainerStatus::cases(), ServerState::cases()))
@@ -109,11 +149,11 @@ class EditServer extends EditRecord
'lg' => 1,
]),
Forms\Components\Textarea::make('description')
->label('Description')
Textarea::make('description')
->label('Notes')
->columnSpanFull(),
Forms\Components\TextInput::make('uuid')
TextInput::make('uuid')
->hintAction(CopyAction::make())
->columnSpan([
'default' => 2,
@@ -123,7 +163,7 @@ class EditServer extends EditRecord
])
->readOnly()
->dehydrated(false),
Forms\Components\TextInput::make('uuid_short')
TextInput::make('uuid_short')
->label('Short UUID')
->hintAction(CopyAction::make())
->columnSpan([
@@ -134,7 +174,7 @@ class EditServer extends EditRecord
])
->readOnly()
->dehydrated(false),
Forms\Components\TextInput::make('external_id')
TextInput::make('external_id')
->label('External ID')
->columnSpan([
'default' => 2,
@@ -143,7 +183,7 @@ class EditServer extends EditRecord
'lg' => 3,
])
->maxLength(255),
Forms\Components\Select::make('node_id')
Select::make('node_id')
->label('Node')
->relationship('node', 'name')
->columnSpan([
@@ -154,10 +194,11 @@ class EditServer extends EditRecord
])
->disabled(),
]),
Tabs\Tab::make('Environment')
Tab::make('Environment')
->icon('tabler-brand-docker')
->schema([
Forms\Components\Fieldset::make('Resource Limits')
Fieldset::make('Resource Limits')
->columns([
'default' => 1,
'sm' => 2,
@@ -165,14 +206,14 @@ class EditServer extends EditRecord
'lg' => 3,
])
->schema([
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_mem')
ToggleButtons::make('unlimited_mem')
->label('Memory')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => 'Unlimited',
@@ -184,9 +225,9 @@ class EditServer extends EditRecord
])
->columnSpan(2),
Forms\Components\TextInput::make('memory')
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label('Memory Limit')->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
@@ -195,15 +236,15 @@ class EditServer extends EditRecord
->minValue(0),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_disk')
ToggleButtons::make('unlimited_disk')
->label('Disk Space')->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => 'Unlimited',
false => 'Limited',
@@ -214,9 +255,9 @@ class EditServer extends EditRecord
])
->columnSpan(2),
Forms\Components\TextInput::make('disk')
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label('Disk Space Limit')->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->required()
@@ -225,14 +266,14 @@ class EditServer extends EditRecord
->minValue(0),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('unlimited_cpu')
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->live()
->options([
true => 'Unlimited',
@@ -244,9 +285,9 @@ class EditServer extends EditRecord
])
->columnSpan(2),
Forms\Components\TextInput::make('cpu')
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->required()
@@ -255,15 +296,15 @@ class EditServer extends EditRecord
->minValue(0),
]),
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('swap_support')
ToggleButtons::make('swap_support')
->live()
->label('Enable Swap Memory')->inlineLabel()->inline()
->columnSpan(2)
->afterStateUpdated(function ($state, Forms\Set $set) {
->afterStateUpdated(function ($state, Set $set) {
$value = match ($state) {
'unlimited' => -1,
'disabled' => 0,
@@ -273,7 +314,7 @@ class EditServer extends EditRecord
$set('swap', $value);
})
->formatStateUsing(function (Forms\Get $get) {
->formatStateUsing(function (Get $get) {
return match (true) {
$get('swap') > 0 => 'limited',
$get('swap') == 0 => 'disabled',
@@ -292,9 +333,9 @@ class EditServer extends EditRecord
'disabled' => 'danger',
]),
Forms\Components\TextInput::make('swap')
TextInput::make('swap')
->dehydratedWhenHidden()
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
->hidden(fn (Get $get) => match ($get('swap_support')) {
'disabled', 'unlimited', true => true,
default => false,
})
@@ -310,11 +351,11 @@ class EditServer extends EditRecord
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion'),
Forms\Components\Grid::make()
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
Forms\Components\ToggleButtons::make('oom_killer')
ToggleButtons::make('oom_killer')
->label('OOM Killer')->inlineLabel()->inline()
->columnSpan(2)
->options([
@@ -326,12 +367,12 @@ class EditServer extends EditRecord
true => 'danger',
]),
Forms\Components\TextInput::make('oom_disabled_hidden')
TextInput::make('oom_disabled_hidden')
->hidden(),
]),
]),
Forms\Components\Fieldset::make('Feature Limits')
Fieldset::make('Feature Limits')
->inlineLabel()
->columns([
'default' => 1,
@@ -340,23 +381,23 @@ class EditServer extends EditRecord
'lg' => 3,
])
->schema([
Forms\Components\TextInput::make('allocation_limit')
TextInput::make('allocation_limit')
->suffixIcon('tabler-network')
->required()
->minValue(0)
->numeric(),
Forms\Components\TextInput::make('database_limit')
TextInput::make('database_limit')
->suffixIcon('tabler-database')
->required()
->minValue(0)
->numeric(),
Forms\Components\TextInput::make('backup_limit')
TextInput::make('backup_limit')
->suffixIcon('tabler-copy-check')
->required()
->minValue(0)
->numeric(),
]),
Forms\Components\Fieldset::make('Docker Settings')
Fieldset::make('Docker Settings')
->columns([
'default' => 1,
'sm' => 2,
@@ -364,10 +405,10 @@ class EditServer extends EditRecord
'lg' => 3,
])
->schema([
Forms\Components\Select::make('select_image')
Select::make('select_image')
->label('Image Name')
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
->options(function ($state, Forms\Get $get, Forms\Set $set) {
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
->options(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
@@ -383,10 +424,10 @@ class EditServer extends EditRecord
->selectablePlaceholder(false)
->columnSpan(1),
Forms\Components\TextInput::make('image')
TextInput::make('image')
->label('Image')
->debounce(500)
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
->afterStateUpdated(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
@@ -406,7 +447,7 @@ class EditServer extends EditRecord
->columnSpanFull(),
]),
]),
Tabs\Tab::make('Egg')
Tab::make('Egg')
->icon('tabler-egg')
->columns([
'default' => 1,
@@ -415,7 +456,7 @@ class EditServer extends EditRecord
'lg' => 5,
])
->schema([
Forms\Components\Select::make('egg_id')
Select::make('egg_id')
->disabledOn('edit')
->prefixIcon('tabler-egg')
->columnSpan([
@@ -429,7 +470,7 @@ class EditServer extends EditRecord
->preload()
->required(),
Forms\Components\ToggleButtons::make('skip_scripts')
ToggleButtons::make('skip_scripts')
->label('Run Egg Install Script?')->inline()
->columnSpan([
'default' => 6,
@@ -451,7 +492,67 @@ class EditServer extends EditRecord
])
->required(),
Forms\Components\Textarea::make('startup')
Forms\Components\TagsInput::make('ports')
->columnSpan(3)
->placeholder('Example: 25565, 8080, 1337-1340')
->splitKeys(['Tab', ' ', ','])
->helperText(new HtmlString('
These are the ports that users can connect to this Server through.
<br />
You would typically port forward these on your home network.
'))
->label('Ports')
->formatStateUsing(fn (Server $server) => $server->ports->map(fn ($port) => (string) $port)->all())
->afterStateUpdated(self::ports(...))
->live(),
Forms\Components\Repeater::make('portVariables')
->label('Port Assignments')
->columnSpan(3)
->addable(false)
->deletable(false)
->mutateRelationshipDataBeforeSaveUsing(function ($data) {
$portIndex = $data['port'];
unset($data['port']);
return [
'variable_value' => (string) $this->ports[$portIndex],
];
})
->relationship('serverVariables', function (Builder $query) {
$query->whereHas('variable', function (Builder $query) {
$query->where('rules', 'like', '%port%');
});
})
->simple(
Forms\Components\Select::make('port')
->live()
->disabled(fn (Forms\Get $get) => empty($get('../../ports')) || empty($get('../../assignments')))
->prefix(function (Forms\Components\Component $component, ServerVariable $serverVariable) {
return $serverVariable->variable->env_variable;
})
->formatStateUsing(function (ServerVariable $serverVariable, Forms\Get $get) {
return array_search($serverVariable->variable_value, array_values($get('../../ports')));
})
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->options(fn (Forms\Get $get) => $this->ports)
->required(),
)
->afterStateHydrated(function (Forms\Set $set, Forms\Get $get, Server $server) {
$this->ports($ports = $get('ports'), $set);
foreach ($this->portOptions($server->egg) as $key => $port) {
$set("assignments.$key", ['port' => $portIndex = array_search($port, array_values($ports))]);
}
}),
Textarea::make('startup')
->label('Startup Command')
->required()
->columnSpan(6)
@@ -462,18 +563,18 @@ class EditServer extends EditRecord
);
}),
Forms\Components\Textarea::make('defaultStartup')
Textarea::make('defaultStartup')
->hintAction(CopyAction::make())
->label('Default Startup Command')
->disabled()
->formatStateUsing(function ($state, Get $get, Set $set) {
->formatStateUsing(function ($state, Get $get) {
$egg = Egg::query()->find($get('egg_id'));
return $egg->startup;
})
->columnSpan(6),
Forms\Components\Repeater::make('server_variables')
Repeater::make('server_variables')
->relationship('serverVariables', function (Builder $query) {
/** @var Server $server */
$server = $this->getRecord();
@@ -502,7 +603,7 @@ class EditServer extends EditRecord
->reorderable(false)->addable(false)->deletable(false)
->schema(function () {
$text = Forms\Components\TextInput::make('variable_value')
$text = TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
->rules([
@@ -519,7 +620,7 @@ class EditServer extends EditRecord
},
]);
$select = Forms\Components\Select::make('variable_value')
$select = Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
@@ -533,17 +634,17 @@ class EditServer extends EditRecord
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable?->description) ? '—' : $serverVariable->variable->description);
}
return $components;
})
->columnSpan(6),
]),
Tabs\Tab::make('Mounts')
Tab::make('Mounts')
->icon('tabler-layers-linked')
->schema([
Forms\Components\CheckboxList::make('mounts')
CheckboxList::make('mounts')
->relationship('mounts')
->options(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
@@ -551,7 +652,7 @@ class EditServer extends EditRecord
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
->columnSpanFull(),
]),
Tabs\Tab::make('Databases')
Tab::make('Databases')
->icon('tabler-database')
->schema([
Repeater::make('databases')
@@ -559,7 +660,7 @@ class EditServer extends EditRecord
->helperText(fn (Server $server) => $server->databases->isNotEmpty() ? '' : 'No Databases exist for this Server')
->columns(2)
->schema([
Forms\Components\TextInput::make('database')
TextInput::make('database')
->columnSpan(2)
->label('Database Name')
->disabled()
@@ -570,11 +671,11 @@ class EditServer extends EditRecord
->icon('tabler-trash')
->action(fn (DatabaseManagementService $databaseManagementService, $record) => $databaseManagementService->delete($record))
),
Forms\Components\TextInput::make('username')
TextInput::make('username')
->disabled()
->formatStateUsing(fn ($record) => $record->username)
->columnSpan(2),
Forms\Components\TextInput::make('password')
TextInput::make('password')
->disabled()
->hintAction(
Action::make('rotate')
@@ -584,30 +685,30 @@ class EditServer extends EditRecord
)
->formatStateUsing(fn (Database $database) => $database->password)
->columnSpan(2),
Forms\Components\TextInput::make('remote')
TextInput::make('remote')
->disabled()
->formatStateUsing(fn ($record) => $record->remote)
->columnSpan(1)
->label('Connections From'),
Forms\Components\TextInput::make('max_connections')
TextInput::make('max_connections')
->disabled()
->formatStateUsing(fn ($record) => $record->max_connections)
->columnSpan(1),
Forms\Components\TextInput::make('JDBC')
TextInput::make('JDBC')
->disabled()
->label('JDBC Connection String')
->columnSpan(2)
->formatStateUsing(fn (Forms\Get $get, $record) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($record->password) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database')),
->formatStateUsing(fn (Get $get, $record) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($record->password) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database')),
])
->relationship('databases')
->deletable(false)
->addable(false)
->columnSpan(4),
])->columns(4),
Tabs\Tab::make('Actions')
Tab::make('Actions')
->icon('tabler-settings')
->schema([
Forms\Components\Fieldset::make('Server Actions')
Fieldset::make('Server Actions')
->columns([
'default' => 1,
'sm' => 2,
@@ -615,11 +716,11 @@ class EditServer extends EditRecord
'lg' => 6,
])
->schema([
Forms\Components\Grid::make()
Grid::make()
->columnSpan(3)
->schema([
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('toggleInstall')
Action::make('toggleInstall')
->label('Toggle Install Status')
->disabled(fn (Server $server) => $server->isSuspended())
->action(function (ServersController $serversController, Server $server) {
@@ -628,14 +729,14 @@ class EditServer extends EditRecord
$this->refreshFormData(['status', 'docker']);
}),
])->fullWidth(),
Forms\Components\ToggleButtons::make('')
ToggleButtons::make('')
->hint('If you need to change the install status from uninstalled to installed, or vice versa, you may do so with this button.'),
]),
Forms\Components\Grid::make()
Grid::make()
->columnSpan(3)
->schema([
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('toggleSuspend')
Action::make('toggleSuspend')
->label('Suspend')
->color('warning')
->hidden(fn (Server $server) => $server->isSuspended())
@@ -645,7 +746,7 @@ class EditServer extends EditRecord
$this->refreshFormData(['status', 'docker']);
}),
Forms\Components\Actions\Action::make('toggleUnsuspend')
Action::make('toggleUnsuspend')
->label('Unsuspend')
->color('success')
->hidden(fn (Server $server) => !$server->isSuspended())
@@ -656,37 +757,37 @@ class EditServer extends EditRecord
$this->refreshFormData(['status', 'docker']);
}),
])->fullWidth(),
Forms\Components\ToggleButtons::make('')
ToggleButtons::make('')
->hidden(fn (Server $server) => $server->isSuspended())
->hint('This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API.'),
Forms\Components\ToggleButtons::make('')
ToggleButtons::make('')
->hidden(fn (Server $server) => !$server->isSuspended())
->hint('This will unsuspend the server and restore normal user access.'),
]),
Forms\Components\Grid::make()
Grid::make()
->columnSpan(3)
->schema([
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('transfer')
Action::make('transfer')
->label('Transfer Soon™')
->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, []))
->disabled() //TODO!
->form([ //TODO!
Forms\Components\Select::make('newNode')
Select::make('newNode')
->label('New Node')
->required()
->options([
true => 'on',
false => 'off',
]),
Forms\Components\Select::make('newMainAllocation')
Select::make('newMainAllocation')
->label('New Main Allocation')
->required()
->options([
true => 'on',
false => 'off',
]),
Forms\Components\Select::make('newAdditionalAllocation')
Select::make('newAdditionalAllocation')
->label('New Additional Allocations')
->options([
true => 'on',
@@ -695,14 +796,14 @@ class EditServer extends EditRecord
])
->modalHeading('Transfer'),
])->fullWidth(),
Forms\Components\ToggleButtons::make('')
ToggleButtons::make('')
->hint('Transfer this server to another node connected to this panel. Warning! This feature has not been fully tested and may have bugs.'),
]),
Forms\Components\Grid::make()
Grid::make()
->columnSpan(3)
->schema([
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('reinstall')
Action::make('reinstall')
->label('Reinstall')
->color('danger')
->requiresConfirmation()
@@ -711,7 +812,7 @@ class EditServer extends EditRecord
->disabled(fn (Server $server) => $server->isSuspended())
->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)),
])->fullWidth(),
Forms\Components\ToggleButtons::make('')
ToggleButtons::make('')
->hint('This will reinstall the server with the assigned egg install script.'),
]),
]),
@@ -725,13 +826,14 @@ class EditServer extends EditRecord
return $form
->columns()
->schema([
Forms\Components\Select::make('toNode')
Select::make('toNode')
->label('New Node'),
Forms\Components\TextInput::make('newAllocation')
TextInput::make('newAllocation')
->label('Allocation'),
]);
}
protected function getHeaderActions(): array
{
return [
@@ -740,11 +842,12 @@ class EditServer extends EditRecord
->color('danger')
->label('Delete')
->requiresConfirmation()
->action(function (Server $server) {
resolve(ServerDeletionService::class)->handle($server);
->action(function (Server $server, ServerDeletionService $service) {
$service->handle($server);
return redirect(ListServers::getUrl());
}),
})
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
Actions\Action::make('console')
->label('Console')
->icon('tabler-terminal')
@@ -753,6 +856,7 @@ class EditServer extends EditRecord
];
}
protected function getFormActions(): array
{
return [];
@@ -769,26 +873,23 @@ class EditServer extends EditRecord
return $data;
}
public function getRelationManagers(): array
{
return [
ServerResource\RelationManagers\AllocationsRelationManager::class,
];
}
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
{
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
if ($component instanceof Forms\Components\Select) {
if (collect($serverVariable->variable->rules)->contains('port')) {
return true;
}
if ($component instanceof Select) {
return !$containsRuleIn;
}
if ($component instanceof Forms\Components\TextInput) {
if ($component instanceof TextInput) {
return $containsRuleIn;
}
throw new \Exception('Component type not supported: ' . $component::class);
throw new Exception('Component type not supported: ' . $component::class);
}
private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
@@ -803,7 +904,77 @@ class EditServer extends EditRecord
->all();
}
protected function rotatePassword(DatabasePasswordService $service, $record, $set, $get): void
public function ports(array $state, Forms\Set $set): void
{
$ports = collect();
foreach ($state as $portEntry) {
if (str_contains($portEntry, '-')) {
[$start, $end] = explode('-', $portEntry);
try {
$startEndpoint = new Endpoint($start);
$endEndpoint = new Endpoint($end);
} catch (Exception) {
continue;
}
if ($startEndpoint->ip !== $endEndpoint->ip) {
continue;
}
foreach (range($startEndpoint->port, $endEndpoint->port) as $port) {
$ports->push(new Endpoint($port, $startEndpoint->ip));
}
for ($i = $start; $i <= $end; $i++) {
$ports->push($i);
}
continue;
}
try {
$ports->push(new Endpoint($portEntry));
} catch (Exception) {
continue;
}
}
$ports = $ports->map(fn ($endpoint) => (string) $endpoint);
$uniquePorts = $ports->unique()->values();
if ($ports->count() > $uniquePorts->count()) {
$ports = $uniquePorts;
}
$set('ports', $ports->all());
$this->ports = $ports->all();
}
public function portOptions(Egg $egg, ?string $startup = null): array
{
if (empty($startup)) {
$startup = $egg->startup;
}
$options = [];
if (str_contains($startup, '{{SERVER_PORT}}')) {
$options['SERVER_PORT'] = null;
}
foreach ($egg->variables as $variable) {
if (!in_array('port', $variable->rules)) {
continue;
}
$options[$variable->env_variable] = $variable->default_value;
}
return $options;
}
protected function rotatePassword(DatabasePasswordService $service, Database $record, Set $set, Get $get): void
{
$newPassword = $service->handle($record);
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database');

View File

@@ -4,13 +4,14 @@ namespace App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use App\Models\Server;
use App\Models\User;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Filament\Tables;
class ListServers extends ListRecords
{
@@ -27,64 +28,55 @@ class ListServers extends ListRecords
Group::make('egg.name')->getDescriptionFromRecordUsing(fn (Server $server): string => str($server->egg->description)->limit(150)),
])
->columns([
Tables\Columns\TextColumn::make('condition')
TextColumn::make('condition')
->default('unknown')
->badge()
->icon(fn (Server $server) => $server->conditionIcon())
->color(fn (Server $server) => $server->conditionColor()),
Tables\Columns\TextColumn::make('uuid')
TextColumn::make('uuid')
->hidden()
->label('UUID')
->searchable(),
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->icon('tabler-brand-docker')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('node.name')
TextColumn::make('node.name')
->icon('tabler-server-2')
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'node.name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('egg.name')
TextColumn::make('egg.name')
->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'egg.name')
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('user.username')
TextColumn::make('user.username')
->icon('tabler-user')
->label('Owner')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'user.username')
->sortable()
->searchable(),
Tables\Columns\SelectColumn::make('allocation_id')
->label('Primary Allocation')
->options(fn (Server $server) => $server->allocations->mapWithKeys(
fn ($allocation) => [$allocation->id => $allocation->address])
)
->selectablePlaceholder(false)
->sortable(),
Tables\Columns\TextColumn::make('image')->hidden(),
Tables\Columns\TextColumn::make('backups_count')
TextColumn::make('image')->hidden(),
TextColumn::make('backups_count')
->counts('backups')
->label('Backups')
->icon('tabler-file-download')
->numeric()
->sortable(),
TextColumn::make('ports')
->badge()
->separator(),
])
->actions([
Tables\Actions\Action::make('View')
Action::make('View')
->icon('tabler-terminal')
->url(fn (Server $server) => "/server/$server->uuid_short")
->visible(function (Server $server) {
/** @var User $user */
$user = auth()->user();
return $user->isRootAdmin() || $user->id === $server->owner_id;
}),
Tables\Actions\EditAction::make(),
->authorize(fn () => auth()->user()->can('view server')),
EditAction::make(),
])
->emptyStateIcon('tabler-brand-docker')
->searchable()
@@ -96,6 +88,7 @@ class ListServers extends ListRecords
->button(),
]);
}
protected function getHeaderActions(): array
{
return [

View File

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

View File

@@ -13,7 +13,9 @@ use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Common\Version;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Closure;
use DateTimeZone;
use Exception;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
@@ -21,13 +23,14 @@ use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\HtmlString;
use Illuminate\Validation\Rules\Password;
@@ -37,6 +40,13 @@ use Illuminate\Validation\Rules\Password;
*/
class EditProfile extends \Filament\Pages\Auth\EditProfile
{
private ToggleTwoFactorService $toggleTwoFactorService;
public function boot(ToggleTwoFactorService $toggleTwoFactorService): void
{
$this->toggleTwoFactorService = $toggleTwoFactorService;
}
protected function getForms(): array
{
return [
@@ -57,7 +67,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->maxLength(255)
->unique(ignoreRecord: true)
->autofocus(),
TextInput::make('email')
->prefixIcon('tabler-mail')
->label(trans('strings.email'))
@@ -65,7 +74,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->required()
->maxLength(255)
->unique(ignoreRecord: true),
TextInput::make('password')
->label(trans('strings.password'))
->password()
@@ -77,7 +85,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->dehydrateStateUsing(fn ($state): string => Hash::make($state))
->live(debounce: 500)
->same('passwordConfirmation'),
TextInput::make('passwordConfirmation')
->label(trans('strings.password_confirmation'))
->password()
@@ -86,13 +93,11 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->required()
->visible(fn (Get $get): bool => filled($get('password')))
->dehydrated(false),
Select::make('timezone')
->required()
->prefixIcon('tabler-clock-pin')
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
->searchable(),
Select::make('language')
->label(trans('strings.language'))
->required()
@@ -110,8 +115,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
Tab::make('2FA')
->icon('tabler-shield-lock')
->schema(function () {
->schema(function (TwoFactorSetupService $setupService) {
if ($this->getUser()->use_totp) {
return [
Placeholder::make('2fa-already-enabled')
@@ -129,8 +133,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
];
}
/** @var TwoFactorSetupService */
$setupService = app(TwoFactorSetupService::class);
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
"users.{$this->getUser()->id}.2fa.state",
@@ -196,16 +198,13 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->helperText('Enter your current password to verify.'),
];
}),
Tab::make('API Keys')
->icon('tabler-key')
->schema([
Grid::make('asdf')->columns(5)->schema([
Grid::make(5)->schema([
Section::make('Create API Key')->columnSpan(3)->schema([
TextInput::make('description')
->live(),
TagsInput::make('allowed_ips')
->live()
->splitKeys([',', ' ', 'Tab'])
@@ -222,12 +221,10 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
$get('description'),
$get('allowed_ips'),
);
Activity::event('user:api-key.create')
->subject($token->accessToken)
->property('identifier', $token->accessToken->identifier)
->log();
$action->success();
}),
]),
@@ -256,13 +253,11 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
]),
]),
]),
Tab::make('SSH Keys')
->icon('tabler-lock-code')
->schema([
Placeholder::make('Coming soon!'),
]),
Tab::make('Activity')
->icon('tabler-history')
->schema([
@@ -286,23 +281,21 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
];
}
protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloquent\Model
protected function handleRecordUpdate(Model $record, array $data): Model
{
if ($token = $data['2facode'] ?? null) {
/** @var ToggleTwoFactorService $service */
$service = resolve(ToggleTwoFactorService::class);
if (!$record instanceof User) {
return $record;
}
$tokens = $service->handle($record, $token, true);
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
if ($token = $data['2facode'] ?? null) {
$tokens = $this->toggleTwoFactorService->handle($record, $token, true);
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), 15);
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
}
if ($token = $data['2fa-disable-code'] ?? null) {
/** @var ToggleTwoFactorService $service */
$service = resolve(ToggleTwoFactorService::class);
$service->handle($record, $token, false);
$this->toggleTwoFactorService->handle($record, $token, false);
cache()->forget("users.$record->id.2fa.state");
}
@@ -310,7 +303,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
return parent::handleRecordUpdate($record, $data);
}
public function exception($e, $stopPropagation): void
public function exception(Exception $e, Closure $stopPropagation): void
{
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
Notification::make()

View File

@@ -18,6 +18,7 @@ use Illuminate\Support\Facades\Hash;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
public function form(Form $form): Form
{
return $form
@@ -46,6 +47,7 @@ class EditUser extends EditRecord
])->columns(),
]);
}
protected function getHeaderActions(): array
{
return [

View File

@@ -78,6 +78,7 @@ class ListUsers extends ListRecords
]),
]);
}
protected function getHeaderActions(): array
{
return [
@@ -110,13 +111,11 @@ class ListUsers extends ListRecords
]),
])
->successRedirectUrl(route('filament.admin.resources.users.index'))
->action(function (array $data) {
->action(function (array $data, UserCreationService $creationService) {
$roles = $data['roles'];
$roles = collect($roles)->map(fn ($role) => Role::findById($role));
unset($data['roles']);
/** @var UserCreationService $creationService */
$creationService = resolve(UserCreationService::class);
$user = $creationService->handle($data);
$user->syncRoles($roles);

View File

@@ -6,10 +6,10 @@ use App\Enums\ServerState;
use App\Models\Server;
use App\Models\User;
use App\Services\Servers\SuspensionService;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tables\Actions;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Actions;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ServersRelationManager extends RelationManager
{
@@ -31,54 +31,48 @@ class ServersRelationManager extends RelationManager
)
->label('Suspend All Servers')
->color('warning')
->action(function () use ($user) {
->action(function (SuspensionService $suspensionService) use ($user) {
foreach ($user->servers()->whereNot('status', ServerState::Suspended)->get() as $server) {
resolve(SuspensionService::class)->toggle($server);
$suspensionService->toggle($server);
}
}),
Actions\Action::make('toggleUnsuspend')
->hidden(fn () => $user->servers()->where('status', ServerState::Suspended)->count() === 0)
->label('Unsuspend All Servers')
->color('primary')
->action(function () use ($user) {
->action(function (SuspensionService $suspensionService) use ($user) {
foreach ($user->servers()->where('status', ServerState::Suspended)->get() as $server) {
resolve(SuspensionService::class)->toggle($server, SuspensionService::ACTION_UNSUSPEND);
$suspensionService->toggle($server, SuspensionService::ACTION_UNSUSPEND);
}
}),
])
->columns([
Tables\Columns\TextColumn::make('uuid')
TextColumn::make('uuid')
->hidden()
->label('UUID')
->searchable(),
Tables\Columns\TextColumn::make('name')
TextColumn::make('name')
->icon('tabler-brand-docker')
->label(trans('strings.name'))
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('node.name')
TextColumn::make('node.name')
->icon('tabler-server-2')
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
->sortable(),
Tables\Columns\TextColumn::make('egg.name')
TextColumn::make('egg.name')
->icon('tabler-egg')
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
->sortable(),
Tables\Columns\SelectColumn::make('allocation.id')
->label('Primary Allocation')
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->sortable(),
Tables\Columns\TextColumn::make('image')->hidden(),
Tables\Columns\TextColumn::make('databases_count')
TextColumn::make('image')->hidden(),
TextColumn::make('databases_count')
->counts('databases')
->label('Databases')
->icon('tabler-database')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('backups_count')
TextColumn::make('backups_count')
->counts('backups')
->label('Backups')
->icon('tabler-file-download')

View File

@@ -127,7 +127,7 @@ class EggController extends Controller
/**
* Normalizes a string of docker image data into the expected egg format.
*/
protected function normalizeDockerImages(string $input = null): array
protected function normalizeDockerImages(?string $input = null): array
{
$data = array_map(fn ($value) => trim($value), explode("\n", $input ?? ''));

View File

@@ -5,7 +5,6 @@ namespace App\Http\Controllers\Admin\Nodes;
use Illuminate\View\View;
use App\Models\Node;
use Illuminate\Support\Collection;
use App\Models\Allocation;
use App\Http\Controllers\Controller;
use App\Traits\Controllers\JavascriptInjection;
use App\Services\Helpers\SoftwareVersionService;
@@ -15,6 +14,7 @@ class NodeViewController extends Controller
use JavascriptInjection;
public const THRESHOLD_PERCENTAGE_LOW = 75;
public const THRESHOLD_PERCENTAGE_MEDIUM = 90;
/**
@@ -56,32 +56,6 @@ class NodeViewController extends Controller
return view('admin.nodes.view.configuration', compact('node'));
}
/**
* Return the node allocation management page.
*/
public function allocations(Node $node): View
{
$node->setRelation(
'allocations',
$node->allocations()
->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL')
->orderByRaw('INET_ATON(ip) ASC')
->orderBy('port')
->with('server:id,name')
->paginate(50)
);
$this->plainInject(['node' => Collection::wrap($node)->only(['id'])]);
return view('admin.nodes.view.allocation', [
'node' => $node,
'allocations' => Allocation::query()->where('node_id', $node->id)
->groupBy('ip')
->orderByRaw('INET_ATON(ip) ASC')
->get(['ip']),
]);
}
/**
* Return a listing of servers that exist for this specific node.
*/

View File

@@ -3,10 +3,7 @@
namespace App\Http\Controllers\Admin;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Models\Node;
use Illuminate\Http\Response;
use App\Models\Allocation;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
@@ -15,11 +12,8 @@ use App\Services\Nodes\NodeUpdateService;
use Illuminate\Cache\Repository as CacheRepository;
use App\Services\Nodes\NodeCreationService;
use App\Services\Nodes\NodeDeletionService;
use App\Services\Allocations\AssignmentService;
use App\Services\Helpers\SoftwareVersionService;
use App\Http\Requests\Admin\Node\NodeFormRequest;
use App\Http\Requests\Admin\Node\AllocationFormRequest;
use App\Http\Requests\Admin\Node\AllocationAliasFormRequest;
class NodesController extends Controller
{
@@ -28,7 +22,6 @@ class NodesController extends Controller
*/
public function __construct(
protected AlertsMessageBag $alert,
protected AssignmentService $assignmentService,
protected CacheRepository $cache,
protected NodeCreationService $creationService,
protected NodeDeletionService $deletionService,
@@ -46,19 +39,6 @@ class NodesController extends Controller
return view('admin.nodes.new');
}
/**
* Post controller to create a new node on the system.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function store(NodeFormRequest $request): RedirectResponse
{
$node = $this->creationService->handle($request->normalize());
$this->alert->info(trans('admin/node.notices.node_created'))->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/**
* Updates settings for a node.
*
@@ -73,83 +53,6 @@ class NodesController extends Controller
return redirect()->route('admin.nodes.view.settings', $node->id)->withInput();
}
/**
* Removes a single allocation from a node.
*
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveSingle(int $node, Allocation $allocation): Response
{
$allocation->delete();
return response('', 204);
}
/**
* Removes multiple individual allocations from a node.
*
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveMultiple(Request $request, int $node): Response
{
$allocations = $request->input('allocations');
foreach ($allocations as $rawAllocation) {
$allocation = new Allocation();
$allocation->id = $rawAllocation['id'];
$this->allocationRemoveSingle($node, $allocation);
}
return response('', 204);
}
/**
* Remove all allocations for a specific IP at once on a node.
*/
public function allocationRemoveBlock(Request $request, int $node): RedirectResponse
{
/** @var Node $node */
$node = Node::query()->findOrFail($node);
$node->allocations()
->where('ip', $request->input('ip'))
->whereNull('server_id')
->delete();
$this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')]))
->flash();
return redirect()->route('admin.nodes.view.allocation', $node);
}
/**
* Sets an alias for a specific allocation on a node.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function allocationSetAlias(AllocationAliasFormRequest $request): \Symfony\Component\HttpFoundation\Response
{
$allocation = Allocation::query()->findOrFail($request->input('allocation_id'));
$alias = (empty($request->input('alias'))) ? null : $request->input('alias');
$allocation->update(['ip_alias' => $alias]);
return response('', 204);
}
/**
* Creates new allocations on a node.
*
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function createAllocation(AllocationFormRequest $request, Node $node): RedirectResponse
{
$this->assignmentService->handle($node, $request->normalize());
$this->alert->success(trans('admin/node.notices.allocations_added'))->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/**
* Deletes a node from the system.
*

View File

@@ -36,11 +36,6 @@ class CreateServerController extends Controller
$eggs = Egg::with('variables')->get();
\JavaScript::put([
'nodeData' => Node::getForServerCreation(),
'eggs' => $eggs->keyBy('id'),
]);
return view('admin.servers.new', [
'eggs' => $eggs,
'nodes' => Node::all(),
@@ -52,7 +47,6 @@ class CreateServerController extends Controller
*
* @throws \Illuminate\Validation\ValidationException
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \Throwable
*/
public function store(ServerFormRequest $request): RedirectResponse

View File

@@ -29,8 +29,6 @@ class ServerTransferController extends Controller
{
$validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
]);
if ($this->transferServerService->handle($server, $validatedData)) {

View File

@@ -47,12 +47,8 @@ class ServerViewController extends Controller
*/
public function build(Server $server): View
{
$allocations = $server->node->allocations->toBase();
return view('admin.servers.view.build', [
'server' => $server,
'assigned' => $allocations->where('server_id', $server->id)->sortBy('port')->sortBy('ip'),
'unassigned' => $allocations->where('server_id', null)->sortBy('port')->sortBy('ip'),
]);
}
@@ -121,10 +117,6 @@ class ServerViewController extends Controller
$canTransfer = true;
}
\JavaScript::put([
'nodeData' => Node::getForServerCreation(),
]);
return view('admin.servers.view.manage', [
'nodes' => Node::all(),
'server' => $server,

View File

@@ -70,7 +70,7 @@ class ServersController extends Controller
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function toggleInstall(Server $server)
public function toggleInstall(Server $server): void
{
if ($server->status === ServerState::InstallFailed) {
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
@@ -84,8 +84,6 @@ class ServersController extends Controller
->body(trans('admin/server.alerts.install_toggled'))
->success()
->send();
return null;
}
/**
@@ -94,7 +92,7 @@ class ServersController extends Controller
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function reinstallServer(Server $server)
public function reinstallServer(Server $server): void
{
$this->reinstallService->handle($server);

View File

@@ -40,7 +40,7 @@ abstract class ApplicationApiController extends Controller
* Perform dependency injection of certain classes needed for core functionality
* without littering the constructors of classes that extend this abstract.
*/
public function loadDependencies(Fractal $fractal, Request $request)
public function loadDependencies(Fractal $fractal, Request $request): void
{
$this->fractal = $fractal;
$this->request = $request;
@@ -51,8 +51,7 @@ abstract class ApplicationApiController extends Controller
*
* @template T of \App\Transformers\Api\Application\BaseTransformer
*
* @param class-string<T> $abstract
*
* @param class-string<T> $abstract
* @return T
*
* @noinspection PhpDocSignatureInspection

View File

@@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Application\Nodes;
use App\Models\Node;
use Illuminate\Http\JsonResponse;
use App\Models\Allocation;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Illuminate\Database\Eloquent\Builder;
use App\Services\Allocations\AssignmentService;
use App\Transformers\Api\Application\AllocationTransformer;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Allocations\GetAllocationsRequest;
use App\Http\Requests\Api\Application\Allocations\StoreAllocationRequest;
use App\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest;
class AllocationController extends ApplicationApiController
{
/**
* AllocationController constructor.
*/
public function __construct(
private AssignmentService $assignmentService,
) {
parent::__construct();
}
/**
* Return all the allocations that exist for a given node.
*/
public function index(GetAllocationsRequest $request, Node $node): array
{
$allocations = QueryBuilder::for($node->allocations())
->allowedFilters([
AllowedFilter::exact('ip'),
AllowedFilter::exact('port'),
'ip_alias',
AllowedFilter::callback('server_id', function (Builder $builder, $value) {
if (empty($value) || is_bool($value) || !ctype_digit((string) $value)) {
return $builder->whereNull('server_id');
}
return $builder->where('server_id', $value);
}),
])
->paginate($request->query('per_page') ?? 50);
return $this->fractal->collection($allocations)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Store new allocations for a given node.
*
* @throws \App\Exceptions\DisplayException
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function store(StoreAllocationRequest $request, Node $node): JsonResponse
{
$this->assignmentService->handle($node, $request->validated());
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Delete a specific allocation from the Panel.
*/
public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): JsonResponse
{
$allocation->delete();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\Application\Roles;
use App\Exceptions\PanelException;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use App\Models\Role;
@@ -21,8 +22,8 @@ class RoleController extends ApplicationApiController
public function index(GetRoleRequest $request): array
{
$roles = QueryBuilder::for(Role::query())
->allowedFilters(['name'])
->allowedSorts(['name'])
->allowedFilters(['id', 'name'])
->allowedSorts(['id', 'name'])
->paginate($request->query('per_page') ?? 10);
return $this->fractal->collection($roles)
@@ -67,6 +68,10 @@ class RoleController extends ApplicationApiController
*/
public function update(UpdateRoleRequest $request, Role $role): array
{
if ($role->isRootAdmin()) {
throw new PanelException('Can\'t update root admin role!');
}
$role->update($request->validated());
return $this->fractal->item($role)
@@ -81,6 +86,10 @@ class RoleController extends ApplicationApiController
*/
public function delete(DeleteRoleRequest $request, Role $role): Response
{
if ($role->isRootAdmin()) {
throw new PanelException('Can\'t delete root admin role!');
}
$role->delete();
return $this->returnNoContent();

View File

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

View File

@@ -69,8 +69,6 @@ class ServerManagementController extends ApplicationApiController
{
$validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
]);
if ($this->transferServerService->handle($server, $validatedData)) {

View File

@@ -14,6 +14,7 @@ use App\Http\Requests\Api\Application\Users\DeleteUserRequest;
use App\Http\Requests\Api\Application\Users\UpdateUserRequest;
use App\Http\Controllers\Api\Application\ApplicationApiController;
use App\Http\Requests\Api\Application\Users\AssignUserRolesRequest;
use App\Models\Role;
class UserController extends ApplicationApiController
{
@@ -79,9 +80,34 @@ class UserController extends ApplicationApiController
/**
* Assign roles to a user.
*/
public function roles(AssignUserRolesRequest $request, User $user): array
public function assignRoles(AssignUserRolesRequest $request, User $user): array
{
$user->syncRoles($request->input('roles'));
foreach ($request->input('roles') as $role) {
if ($role === Role::getRootAdmin()->id) {
continue;
}
$user->assignRole($role);
}
$response = $this->fractal->item($user)
->transformWith($this->getTransformer(UserTransformer::class));
return $response->toArray();
}
/**
* Removes roles from a user.
*/
public function removeRoles(AssignUserRolesRequest $request, User $user): array
{
foreach ($request->input('roles') as $role) {
if ($role === Role::getRootAdmin()->id) {
continue;
}
$user->removeRole($role);
}
$response = $this->fractal->item($user)
->transformWith($this->getTransformer(UserTransformer::class));

View File

@@ -41,8 +41,7 @@ abstract class ClientApiController extends ApplicationApiController
*
* @template T of \App\Transformers\Api\Client\BaseClientTransformer
*
* @param class-string<T> $abstract
*
* @param class-string<T> $abstract
* @return T
*
* @noinspection PhpDocSignatureInspection

View File

@@ -1,137 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Client\Servers;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
use App\Facades\Activity;
use App\Models\Allocation;
use App\Exceptions\DisplayException;
use App\Transformers\Api\Client\AllocationTransformer;
use App\Http\Controllers\Api\Client\ClientApiController;
use App\Services\Allocations\FindAssignableAllocationService;
use App\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
use App\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest;
use App\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest;
use App\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest;
use App\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
class NetworkAllocationController extends ClientApiController
{
/**
* NetworkAllocationController constructor.
*/
public function __construct(
private FindAssignableAllocationService $assignableAllocationService,
) {
parent::__construct();
}
/**
* Lists all the allocations available to a server and whether
* they are currently assigned as the primary for this server.
*/
public function index(GetNetworkRequest $request, Server $server): array
{
return $this->fractal->collection($server->allocations)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Set the primary allocation for a server.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(UpdateAllocationRequest $request, Server $server, Allocation $allocation): array
{
$original = $allocation->notes;
$allocation->forceFill(['notes' => $request->input('notes')])->save();
if ($original !== $allocation->notes) {
Activity::event('server:allocation.notes')
->subject($allocation)
->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes])
->log();
}
return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Set the primary allocation for a server.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, Allocation $allocation): array
{
$server->allocation()->associate($allocation);
$server->save();
Activity::event('server:allocation.primary')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Set the notes for the allocation for a server.
*s.
*
* @throws \App\Exceptions\DisplayException
*/
public function store(NewAllocationRequest $request, Server $server): array
{
if ($server->allocations()->count() >= $server->allocation_limit) {
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
}
$allocation = $this->assignableAllocationService->handle($server);
Activity::event('server:allocation.create')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
return $this->fractal->item($allocation)
->transformWith($this->getTransformer(AllocationTransformer::class))
->toArray();
}
/**
* Delete an allocation from a server.
*
* @throws \App\Exceptions\DisplayException
*/
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation): JsonResponse
{
// Don't allow the deletion of allocations if the server does not have an
// allocation limit set.
if (empty($server->allocation_limit)) {
throw new DisplayException('You cannot delete allocations for this server: no allocation limit is set.');
}
if ($allocation->id === $server->allocation_id) {
throw new DisplayException('You cannot delete the primary allocation for this server.');
}
Allocation::query()->where('id', $allocation->id)->update([
'notes' => null,
'server_id' => null,
]);
Activity::event('server:allocation.delete')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@@ -2,16 +2,16 @@
namespace App\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Response;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
use App\Facades\Activity;
use App\Services\Servers\ReinstallServerService;
use App\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
use App\Http\Requests\Api\Client\Servers\Settings\RenameServerRequest;
use App\Http\Requests\Api\Client\Servers\Settings\SetDockerImageRequest;
use App\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest;
use App\Models\Server;
use App\Services\Servers\ReinstallServerService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class SettingsController extends ClientApiController
{
@@ -33,7 +33,11 @@ class SettingsController extends ClientApiController
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
$server->name = $name;
$server->description = $description;
if (config('panel.editable_server_descriptions')) {
$server->description = $description;
}
$server->save();
if ($server->name !== $name) {

View File

@@ -14,7 +14,7 @@ use App\Http\Requests\Api\Remote\ActivityEventRequest;
class ActivityProcessingController extends Controller
{
public function __invoke(ActivityEventRequest $request)
public function __invoke(ActivityEventRequest $request): void
{
$tz = Carbon::now()->getTimezone();

View File

@@ -16,7 +16,7 @@ class ServerContainersController extends Controller
{
$status = fluent($request->json()->all())->get('data.new_state');
cache()->set("servers.$server->uuid.container.status", $status, now()->addHour());
cache()->set("servers.$server->uuid.container.status", $status, 3600);
return new JsonResponse([]);
}

View File

@@ -48,7 +48,7 @@ class ServerDetailsController extends Controller
// Avoid run-away N+1 SQL queries by preloading the relationships that are used
// within each of the services called below.
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables')
$servers = Server::query()->with('egg', 'mounts', 'variables')
->where('node_id', $node->id)
// If you don't cast this to a string you'll end up with a stringified per_page returned in
// the metadata, and then daemon will panic crash as a result.

View File

@@ -6,7 +6,6 @@ use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use App\Models\Allocation;
use App\Models\ServerTransfer;
use Illuminate\Database\ConnectionInterface;
use App\Http\Controllers\Controller;
@@ -53,13 +52,7 @@ class ServerTransferController extends Controller
/** @var \App\Models\Server $server */
$server = $this->connection->transaction(function () use ($server, $transfer) {
$allocations = array_merge([$transfer->old_allocation], $transfer->old_additional_allocations);
// Remove the old allocations for the server and re-assign the server to the new
// primary allocation and node.
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
$server->update([
'allocation_id' => $transfer->new_allocation,
'node_id' => $transfer->new_node,
]);
@@ -93,9 +86,6 @@ class ServerTransferController extends Controller
{
$this->connection->transaction(function () use (&$transfer) {
$transfer->forceFill(['successful' => false])->saveOrFail();
$allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations);
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
});
return new JsonResponse([], Response::HTTP_NO_CONTENT);

View File

@@ -51,7 +51,7 @@ abstract class AbstractLoginController extends Controller
*
* @throws \App\Exceptions\DisplayException
*/
protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null, string $message = null)
protected function sendFailedLoginResponse(Request $request, ?Authenticatable $user = null, ?string $message = null): never
{
$this->incrementLoginAttempts($request);
$this->fireFailedLoginEvent($user, [
@@ -91,7 +91,7 @@ abstract class AbstractLoginController extends Controller
/**
* Determine if the user is logging in using an email or username.
*/
protected function getField(string $input = null): string
protected function getField(?string $input = null): string
{
return ($input && str_contains($input, '@')) ? 'email' : 'username';
}
@@ -99,7 +99,7 @@ abstract class AbstractLoginController extends Controller
/**
* Fire a failed login event.
*/
protected function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = [])
protected function fireFailedLoginEvent(?Authenticatable $user = null, array $credentials = []): void
{
Event::dispatch(new Failed('auth', $user, $credentials));
}

View File

@@ -16,7 +16,7 @@ class ForgotPasswordController extends Controller
/**
* Get the response for a failed password reset link.
*/
protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse
protected function sendResetLinkFailedResponse(Request $request, string $response): JsonResponse
{
// As noted in #358 we will return success even if it failed
// to avoid pointing out that an account does or does not
@@ -28,10 +28,8 @@ class ForgotPasswordController extends Controller
/**
* Get the response for a successful password reset link.
*
* @param string $response
*/
protected function sendResetLinkResponse(Request $request, $response): JsonResponse
protected function sendResetLinkResponse(Request $request, string $response): JsonResponse
{
return response()->json([
'status' => trans($response),

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