Compare commits

..

137 Commits

Author SHA1 Message Date
Charles
7cc4358a04 Fix 500 on duplicate ports (#861)
* Fix 500 on duplicate ports

This should also address N+1 issues from the last PR

* Combine into one method

* Pint

* Add missing type

* Add 0.0.0.0

* Add notifications to help the user

* Pint

* Too verbose

* Show notification here

* Simplify code

* Reset the ports if the ip changes

* Don’t limit these anymore

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2025-01-04 22:30:37 -05:00
MartinOscar
168d37b996 Add missing externalId on Server creation (#859)
* Add missing externalId on server creation

* Pint

* Fix mobile layout

* fix layout

---------

Co-authored-by: notCharles <charles@pelican.dev>
2025-01-04 19:58:51 +01:00
MartinOscar
df615f6915 Remove validated override (#860) 2025-01-04 13:36:22 -05:00
Charles
17805f676e Add OAuth Settings to Settings (#839)
* Replace tabler icon package

* Use new filled icons

note: not everything has a filled icon

* Add OAuth Settings to Settings Page

* Fix authentik base url

* replace hard coded oauth
2025-01-04 12:35:07 -05:00
Lance Pioch
23d515c3e5 Convert to bytes beforehand (#857) 2025-01-04 12:34:26 -05:00
Charles
7a5dd87385 Change limits section on front end (#853)
* Edit Front end settings

* Use helpers

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2025-01-04 11:48:26 -05:00
Charles
8f51502c6d Remove First/Last Name for Users (#855)
* Update Tests

* Update Translations

* Add Migration

* Remove First/Last Names
2025-01-03 17:13:44 -05:00
MartinOscar
9d48799c28 Remove required (#852) 2025-01-02 23:36:36 +01:00
Lance Pioch
133c1a511f Replace some guzzle exceptions and fix server creation failures (#848)
* Replace guzzle exceptions

* Pint fixes

* Fix test

* Remove unused imports

* Catch & Notify the user instead of 500

* Update app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php

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

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-01-01 15:20:16 -05:00
Lance Pioch
3a7ddfca5e Scope power buttons to current server (#849)
* Scope setServerState to current server

* Use match statement

* Reset this
2025-01-01 15:20:02 -05:00
MartinOscar
00ae3b8b61 Hide Startup + Show Activity on Server panel when in conflictState (#850)
* Hide startup if isInConflictState

* Show ActivityLog regardless of isInConflictState

* Update app/Filament/Server/Pages/Startup.php

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

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-12-31 14:19:18 +01:00
MartinOscar
b5733715a6 Remove useless rightJoin (#851) 2024-12-31 14:05:14 +01:00
Lance Pioch
9a859cdec3 Move role resource under the advanced settings (#847) 2024-12-29 18:15:25 -05:00
MartinOscar
1571e3cb24 Rework Schedules (#843) 2024-12-28 16:03:21 -05:00
Charles
a8680c7aed Mobile + Layout Changes (#836)
* Update Server Listing

* Update Edit/Create Server Pages

Re-arrange limits, CPU->Memory->Disk

* Remove auto focus

its cancer on mobile...

* Hide Title, Quick yaml fix

* Hide columns on mobile

* Hide backup locked on mobile

* Fix schedules for mobile

* Hide Notes on mobile

* Consolidate and clean these up

* Simplify

* Remove unused imports

* Replace tabler icon package

* Update app/Filament/Server/Resources/FileResource/Pages/EditFiles.php

Co-authored-by: Lance Pioch <git@lance.sh>

* Allow the unit to be changed

* Use existing method

* Update composer and pint

* Update resources/views/tables/columns/server-entry-column.blade.php

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

* Simplify html and add small margin

* Unused

* Add enum

---------

Co-authored-by: Lance Pioch <git@lance.sh>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-12-28 16:02:24 -05:00
Scai
66a17879a0 fix: use options instead relationship (#845) 2024-12-27 16:14:05 -05:00
Scai
f684da997c Fix deleting node with database host
* fix: delete database host when node is deleted

* chore: revert change to file
2024-12-27 16:12:49 -05:00
Boy132
00644c2c60 Health page (#469)
* add spatie health

* change slug for health page

* add check for panel version

* only check for debug mode if env isn't local

* add check for node versions

* improve short summary

* fix outdated check

* run pint

* fix health checks during tests

* add count to ok message

* fix typo

* temp fix for phpstan job

* fix pint...

* improve "outdated" count

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

* run pint

* skip node versions check if no nodes are created

* auto run health checks if they didn't run before

* small refactor

* update navigation

Co-authored-by: Charles <sir3lit@gmail.com>

* fix errors if tests didn't run yet

* fix disk usage check

* remove plugin and use own page

* use health status indicator from spatie

* fix after merge

* update icon

* update color classes

* fix after merge

* add back imports

oops...

* wrong import

oops²...

* update spatie/laravel-health to latest

* move Health page to correct namespace

* update NodeVersionsCheck

* use style instead of tailwind classes

workaround until we have vite

* cleanup custom checks

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: Charles <sir3lit@gmail.com>
2024-12-24 19:09:16 +01:00
Boy132
02a0c5c3eb Fix wrong language formatting in charts (#832) 2024-12-17 13:08:12 +01:00
Boy132
993e2c4244 Add CreateUser page (#825) 2024-12-13 09:21:37 +01:00
pelican-vehikl
7a4c4ce02a Listen to more framework webhook events (#728)
* Add new framework events to listen to

* Add simple test for framework events

* Update app/Models/WebhookConfiguration.php

Co-authored-by: Lance Pioch <git@lance.sh>

* Update app/Models/WebhookConfiguration.php

Co-authored-by: Lance Pioch <git@lance.sh>

* Update app/Models/WebhookConfiguration.php

---------

Co-authored-by: Vehikl <go@vehikl.com>
Co-authored-by: Lance Pioch <git@lance.sh>
2024-12-13 01:03:35 -05:00
Boy132
914f3dcdbd Add own action class for "rotate database password" (#822) 2024-12-12 18:34:52 +01:00
Boy132
d43b99792f (Admin) UI Consistency (#824)
* update phpdocs

* replace deprecated $label and $pluralLabel

* update record title attributes and labels

* update create pages

* run pint
2024-12-12 18:26:37 +01:00
Boy132
771eece01e Properly handle 404 for editing files (#816) 2024-12-12 18:26:01 +01:00
Boy132
026494c353 Catch correct Exceptions when updating/ deleting subusers (#828) 2024-12-12 17:32:39 +01:00
Charles
663b097d22 Add Edit/Delete on Tasks (#826) 2024-12-12 10:31:33 -05:00
Boy132
d09227659e Add database notifications (#817)
* add database notifications to all panels

* add successful param to Installed event

* add listener for Installed event

* create event for subuser creation

* add listener for SubUserAdded event

* always send Installed event

* create event for subuser removal

* add listener for SubUserRemoved event

* add prefix to server name

* remove view action from SubUserRemoved notification
2024-12-12 14:38:45 +01:00
Boy132
eb819032bc Add own action classes for egg actions (+ add empty state) (#823)
* add own action classes for egg actions

* add empty state to ListEggs

* put Import before Create
2024-12-12 14:29:02 +01:00
Boy132
5af507b54b Add own column class for node health (#820) 2024-12-12 14:14:52 +01:00
Boy132
bbee45592f Move custom columns to new namespace (#821) 2024-12-12 14:14:37 +01:00
Boy132
640ff9f5b3 Remove unused DatabaseResource (#819) 2024-12-12 14:03:16 +01:00
Charles
d6f814b7a3 Move schedule buttons (#815)
* Move buttons around

* change to Save
2024-12-10 17:57:06 -05:00
Charles
8a122fa99c Add redirect after save (#813) 2024-12-10 17:43:23 -05:00
Boy132
3ffb54503f Custom error pages (#810)
* add custom error pages

* move icon in front of header text

* show exception message if user is root admin

* add missing page for very important error: 418

* Update resources/views/errors/layout.blade.php

* Update resources/views/errors/layout.blade.php

* add dark mode to error pages

---------

Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2024-12-10 23:42:43 +01:00
Charles
53460b8d1b Update File Manager (#814)
* Make Everything Sortable

* Replace app calls
2024-12-10 17:40:11 -05:00
Boy132
0051d9fefc Allow admins to change server egg (#811)
* add service that handles egg changing

* add "change egg" action to EditServer page

* add toggle for keeping old variables or not
2024-12-10 23:38:40 +01:00
Lance Pioch
ef1ae72d06 Dynamic server status (#803)
* Better readability

* Force refresh the server instance

* Use kebab case for these

* Fix phpstan

* Retry a little longer

* Updates

* Add pint

* Don’t need this

* Pint fix
2024-12-10 17:36:14 -05:00
Boy132
3dfdc70790 Make use of Laravels AboutCommand (#809)
* add pelican info to laravel AboutCommand

* simplify p:info command
2024-12-10 23:07:59 +01:00
Boy132
8460c52534 Add Run now button for schedules & add status field (#806)
* add `Run Now` button to schedules

* add status to schedule view/ edit

* only show status on "view"
2024-12-09 23:31:03 +01:00
Lance Pioch
2bfc788e13 Allow searching for port when associating allocations (#801) 2024-12-08 16:24:00 -05:00
Lance Pioch
839ff96271 Fix power buttons (#799) 2024-12-08 16:19:15 -05:00
Lance Pioch
5d2b892eab Better IP addresses (#800)
* Unique ip addresses

* Only ipv4 addresses for now

* Switch to selects
2024-12-08 16:19:04 -05:00
MartinOscar
c953b97009 Force width (#798) 2024-12-08 20:27:16 +01:00
MartinOscar
9716b1e64d Only allow one * (#797) 2024-12-08 20:23:37 +01:00
Boy132
8358e410dc Move installer to correct namespace (#795) 2024-12-08 19:57:00 +01:00
Boy132
f6c586bf5b Add persistFiltersInSession to server list (#796) 2024-12-08 19:14:56 +01:00
Charles
feadaa2caf Add Kill button to console (#791)
* Add Kill button to console

* Add confirm, and warning
2024-12-08 12:01:44 -05:00
Charles
23246eb134 Fix #784 (#790)
* Remove +1

* Update app/Filament/Server/Pages/Settings.php

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-12-08 11:47:01 -05:00
Charles
6921c8b350 Fix power actions (#789) 2024-12-08 17:34:47 +01:00
Charles
8cc91b0747 Small updates (#787)
* increase action size on console

* fix layout on create database
2024-12-08 11:19:35 -05:00
Charles
157fa45234 Fix forever expanding code editor (#782)
* Update CSS

* Update Placeholder
2024-12-07 22:44:13 -05:00
Charles
fd5016809a Enable Global Search (#783) 2024-12-07 22:43:44 -05:00
Lance Pioch
a0f5ef13d6 Show login failure message (#781)
* Show login failure message

* Update resources/scripts/components/auth/LoginContainer.tsx

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

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-12-07 22:03:56 -05:00
Lance Pioch
67f1e91236 Fix Issue 763 (#780)
* Fix path to moved component

* Fix actual issue #763
2024-12-07 20:44:41 -05:00
Charles
cc3a7a2d0d Update Server Console, Again... (#776)
* More console changes

* Update Console Page

* Edit console input

* oops

* Remove failed attempt of clearing input when server offline

* Update File Editor to match console

* a touch more style

* Show not-allowed on read-only input

* round bottom corners of command input

* Move custom css to file
2024-12-07 19:22:18 -05:00
Charles
d908fb9a9d Remove unsaved changes alert (#778)
* Remove unsaved changes alert

* Remove this also

* Remove this also...
2024-12-07 11:39:00 -05:00
Boy132
6b96c9dbda Fix admin discover (#777) 2024-12-07 17:06:57 +01:00
Boy132
e27f23b1b6 Move admin pages & resources into own namespace (#741)
* move admin pages & resources into own namespace

* fix imports for resource pages
2024-12-07 15:51:27 +01:00
Charles
4ad2997566 Update database creation (#775)
Updates database creation flow to account for new database host to many nodes change.
2024-12-06 22:46:36 -05:00
pelican-vehikl
7e7f0be7df Allow Database Hosts to have multiple Nodes (#767)
* WIP

* Update laravel and migrations

* WIP

* fix tests

* Update composer

* Fix transformer

* Fix filament pages

* WIP

* Update DatabaseHostTransformer

* fix: tests

* pint this files pls

* resolve merge better

* Update migration

* Update Migration, Again

* Update down migration

---------

Co-authored-by: Vehikl <go@vehikl.com>
2024-12-06 20:24:30 -05:00
Boy132
5b3ae995e6 Show full client api key after creation (#771)
* show notification when api key is created

* remove hardcoded redirect url
2024-12-06 16:31:58 -05:00
Charles
2a34795ab1 More console changes (#774) 2024-12-06 16:21:05 -05:00
Charles
d3da1b0a58 Update Server Console, Address Overflows (#764)
* Update Console

Updates console to be more better <3.

Light Mode still needs some love, haven't figured that out with filaments light/dark options yet as it does not use the "bright<color>" colors...

* Add overflow to... Everything?

* Oops, Add Name label back

* Actually handle Transfer Status & remove useless switch

* Use switch case

* Readonly command input if server can't receive one

* lint

* Update app/Filament/Server/Widgets/ServerConsole.php

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

* Use filament::icon instead of raw svg

* Update resources/views/filament/components/server-console.blade.php

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

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2024-12-06 09:46:10 -05:00
Boy132
5317f97870 Display roles as badge in user list (#772) 2024-12-06 13:02:37 +01:00
Boy132
b50acfdba2 Add config value for display width to other pages (#770) 2024-12-06 09:45:06 +01:00
Charles
066bdbdf78 Server Listing tweaks. (#760)
* Server Listing tweaks.

* Use filament::icon instead of raw svg & add hover title

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-12-05 08:34:47 -05:00
MartinOscar
8103ba6338 Remove unique (#765) 2024-12-05 08:55:04 +01:00
Boy132
44b879215f Add filters to server list (#762)
* add server owner filter

* add egg filter

* replace SelectFilter with TernaryFilter
2024-12-05 08:31:34 +01:00
Charles
d2a7d7708c Add Display Width setting (#759)
Allow users to pick how "wide" the panel is.
2024-12-04 09:41:47 -05:00
Boy132
efc37dd45a Hide sidebar on server list (#761) 2024-12-04 09:50:49 +01:00
MartinOscar
09eac71f05 Delete subuser on owner change (#748)
* Delete subuser on owner change

* Move logic to Model
2024-12-03 23:55:02 +01:00
Boy132
6d42a15ec3 Handle token expiring and token expired websocket events (#755)
* handle `token expiring` and `token expired` events

* fix "getToken"

* Move logic to Widget instead of blade & add user check

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-12-03 23:54:40 +01:00
Boy132
bbfdee356b Fix timezone for file timestamps (#757) 2024-12-03 14:27:06 +01:00
Charles
994852ca00 Tweak EditNode layout for mobile (#752)
* Tweak EditNode layout for mobile

* Replace hidden with toggle
2024-12-03 05:30:04 -05:00
MartinOscar
141baeb035 Empty array if user->oauth is null (#754) 2024-12-03 08:48:47 +01:00
Boy132
bd51191da6 Add role permissions for webhooks (#742) 2024-12-02 23:53:35 +01:00
Boy132
1337767049 Small changes for new client area (#751)
* add placeholder to allocation notes

* add button to open server in admin area

* use new client area for "console" button on EditServer

* hide schedule presets on view

* use arrow functions for auth checks

* add placeholder to schedules last run

* change icon of "open in admin"

* fix parentheses
2024-12-02 22:27:35 +01:00
Boy132
918ba02075 Remove exception methods because of memory bombing (#750)
* remove exception methods

* throw Halt instead of return

* manually throw Halt to make phpstan happy
2024-12-02 22:27:25 +01:00
Charles
c6977e57c8 Fix Subuser issues. (#747)
* Better Error handling

* Remove unique, make email lowercase in request

* Remove 'kill' option, not used.

* Prevent users from editing them selves

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-12-01 20:17:27 -05:00
MartinOscar
6d1c153d09 Add config panel.editable_server_descriptions check (#734)
* Add config panel.editable_server_descriptions check

* Hide the field rather then disabling it
2024-12-01 23:27:07 +01:00
Boy132
e5433b7aab Auto update resources on server list (#737)
* auto update resources on server list

* use Arr::get helper
2024-12-01 18:12:58 +01:00
Boy132
355810c549 Combine status & uptime, add address on ServerOverview (#739) 2024-12-01 18:12:28 +01:00
Boy132
4fd1937c54 Hide global search button for now (#738) 2024-12-01 18:04:24 +01:00
Lance Pioch
fea1c51337 feat: Client UI translate to Filament (from React) (#416)
* Add new panel

* Add some basic resource pages

* Wip

* Wip terminal

* Wip

* Add new panel

* Add some basic resource pages

* Wip

* [Sub-Users] Add Invite

TODO: The logic with permissions

* [Sub-Users] Fix Creation

* [Cron] Add basics

* Add basic auth and messages

* Add basic buttons

* WIP on issue/353

* WIP on issue/353

* Add Database page

* Update Database Page

* Start of Backup Page

* Composer Update

* Changes

* Send input

* Remove this includes

* Better offline handling

* Consolidate top nav config

* Update Backups Page

* Update Backups

* Change name

* Add Assign All, Layout Fixes.

* conflict

* update schedule pages

* fix phpstan

* update pint.json

* add cron presets to schedule

* fix tests

* fix task creation

* schedules: disable task creation if limit is reached & disable backup action if backup limit is 0

* update activity pages

* update resources

* Update Edit User

TODO: actually save permissions when they're changed.
TODO: Figure out why Control does not update it's state... but the rest do...

* .... Sure it works.

TODO: Update permissions when you save editing a sub user.

* user: update canAccessPanel & canAccessTenant

* add helper to convert bytes into readable format

* very basic file explorer

* files: fix some stuff & remove dummy data

* files: better error handling

* files: basic file editor

* files: add some actions

* File manager updates

* files: fix paths

* Revery Composer Upgrade, Fixes SQLite

* fix: Pint (#517)

feat: MenuItems to and from admin

* Update File Editing

Updated File Editing to its own page,
Added Permission checks for file manager.

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

* add enum for editor langs

* files: add upload & pull actions

* fix build

* files: handle images

* Update to Filament v3.2.98

* files: add remaining actions

* use `authorize` instead of `hidden`

* fix canAccessTenant

* update date columns

* files: testing & fixes

* Fix File Names

Co-authored-by: lancepioch <git@lance.sh>

* Combine Pull/Upload

* Fix BulkDelete

* Uncontained tabs

* Hide Lang Selection, Move Actions

* Update Monaco, more custom

* Add livewire config

livewire limits uploads to 12MB... who knows why...
Fixed uploading a single files failing

* files: fix record url

* basic setup for settings & startup page

* make abstract class for simple app pages

* Basic Startup Page

* Update nav sort

* small cleanup

* startup: fix shouldHideComponent & getSelectOptionsFromRules

* startup: fix non editable fields & set default value

* startup: add todo for save button

* Save Variables after update & off click

Variables update when the user clicks off the input.

* Notifications are cool

* Add rule validation

* Sort variables by sortid

* pint

* Settings Page + Startup Changes

* settings: cleanup

* refactor: use server model for ServerFormPage (formerly known as SimplePage)

* Use Repeater for variables

* Add Network, Remove breadcrumbs

* Add paginated to file explorer

* Fix updating variables

* Add link to go to new client area

* fix after merge

* Add graphs to console page

Graphs still need to get the data from the web socket.

* fix pint & phpstan

* fix authorizeAccess for EditFiles and Startup page

* Fix rules on startup page

* Update console size

* Fix node name

* add "global search" to files list

requires https://github.com/pelican-dev/wings/pull/44

* remove debug dummy data

* update view action on ListServers

* enable SPA mode for app panel

* remove colors from app panel

they are defined globally in AppServiceProvider

* update global search ui a bit

(to be replaced with a custom page that is similar to the list files table)

* add own page for global search

untested - and route needs cleanup (if possible)

* fix File getRows

* remove "path" from SearchFiles (for now)

* fix caching for searched files

* add title and breadcrumbs to global search page

* make cpu & memory charts on console page working

* fix phpstan

* add missing import

* cleanup console views & widgets

* add overview stats to console

* don't be so lazy, console!

* make history working

* decode data to get array

* add missing On

* fix json_decode

* change polling to 1 sec

* hide "0" cpu/ memory

* add data to network chart

* Remove data labels

* fix data on network chart

* fix data on network chart (2nd try)

* WIP Network Stats

* Remove test

* Change MaxWidth

* run pint

* fix phpstan

* Fix storeStats cast

* make $data a string

this time for real

* update visible check for "admin" menu item

* remove account widget

* rebrand "Dashboard" to "Server List"

WIP - doesn't look good but is somewhat working

* fix canAccessPanel

* separate server list into own panel

* change path to avoid conflicts with old client area (and remove sidebar width)

* display correct icon and color on server list entries

* show total memory if server is offline

* replace custom server list page with ListRecords page

* fix tests

* fix namespace

* remove "open" button and make whole column clickable

* Update EditProfile

* run pint

* fix access to server list

* add new login page to panels

* fix next_run_at for new schedules

* use new DateTimeColumn

* add own column for file bytes

* return to server list when clicking title

* fix console loading

* handle server with "conflict state"

* add banner if server is in "conflict state"

* fix phpstan

* update docker image select

* fix permission checks on Settings & Startup pages

* fix query for activity log page

* fix activity log not being logged

* adjust ListActivities

* fix phpstan

* fix pint

* fix profile menu item link on server panel

* add ip tooltip to activity logs (and role permission)

* change backup icon

* update navigation sort

* general code cleanup

* more cleanup

* Disable Restart/Stop if server is offline

* Change rename notification

* Remove negation on abort_unless

* Add notification on save

* Single disabled closure & comment unused import

* Add required to Server Name & Nullable to description

* mutateFormDataBeforeSave doesn't work since we use forceFill

* Fix web socket connection not existing.

* Fix some subuser permissions

* add permission checks to resources

* do not allow self-deletion

* Update editing file permissions

* Fix of the previous fix

* add service for subuser updating

* Only allow save if they have file_update

* Remove unused import

* Update backup delete button

* Add Delete, remove bulks

* Update Database page

* Use Allocation Permissions

* add canAccess check to startup

* Add Permission checks to Settings page

* add service for subuser deletion

* Remove Kill permission

* Updates

* fix move files

* add redirects

* fix phpstan

* activity: remove properties from tans for now

* If alias, use that, else ip

---------

Co-authored-by: notCharles <charles@pelican.dev>
Co-authored-by: Boy132 <mail@boy132.de>
Co-authored-by: Senna <62171904+Poseidon281@users.noreply.github.com>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-12-01 04:13:45 +01:00
Boy132
e0c6137b92 Installer: Shows errors on submit & move runMigrations (#722)
* catch Halt exception to make sure error notifications are displayed

* run migrations on submit to make sure the correct data is used
2024-12-01 04:04:40 +01:00
Charles
cd448cd9a7 Add Create Database btn on admin side (#721)
* Add Create Database btn on admin side

* Remove unused function

* readd function

* replace refreshform function

* add authorize, remove database limit check

* add random words, use proper name function, catch exceptions on creation

* add validation, match old client area more

* Add more authorize to Database tab

* Add confirmation to delete

* make password hidden / revealable

* better clarification

* Set default and remove placeholder.

* Remove server import, add database model to auth

* Make same changes for the database host page

* Update app/Filament/Resources/ServerResource/Pages/EditServer.php

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

* Update app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php

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

* Update app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php

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

* Remove each hidden

* Return nothing if user has no perms

* This is the way... Im done messing with it...

* Fix view permission for relationship manager

* Update app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php

* Pint

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-11-30 22:04:10 -05:00
Boy132
b208835ed4 Add Oauth frontend and backend improvements (#718)
* better oauth provider loading

* add auth frontend

* add configs for all default providers

* add more default providers

* add env variables to enable oauth providers

* small refactor to link/ unlink routes

* add oauth tab to (admin) profile

* use redirects instead of exceptions

* add notification if no oauth user is found

* use import in config

* remove whmcs provider

* replace hardcoded links with `route`

* redirect to account page on unlink

* remove unnecessary controller and handle linking/ unlinking in action

* only show oauth tab if at least one oauth provider is enabled
2024-11-30 17:38:38 +01:00
MartinOscar
951fc73363 Add min length check (#730) 2024-11-27 09:02:41 +01:00
Boy132
ad9447e974 Add back force https (#726) 2024-11-26 23:27:58 +01:00
Boy132
d2d960ecf3 Update egg jsons (#725)
* re-import eggs

* re-import eggs (again)
2024-11-23 23:29:37 +01:00
Boy132
d555c42644 Update all dependencies (#712)
* update composer.lock

* run pint

* fix phpstan

* update migrations (sqlite `dropForeign`)

* fix migrations

* Reset these back for now

* Alphabetize the rules

* run `php artisan filament:upgrade`

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2024-11-22 09:27:57 +01:00
MartinOscar
f33f91698e Add exit admin to Menu (#723) 2024-11-21 17:49:19 +01:00
Boy132
90afae79db Fix permission check if user is subuser and admin (#720) 2024-11-20 08:41:37 +01:00
Boy132
54039e25a4 Make sure UTC is always used internally (#713)
* force app timezone to be UTC

* remove asDateTime overwrite

* add custom column to display dates in user timezone

* use `APP_TIMEZONE` as default timezone for new users

* revert accidental pinting
2024-11-15 20:41:33 +01:00
Boy132
408897cfcf Allow username on filament login page + make case insensitive (#714)
* allow login with username

* make login case insensitive

* fix tests
2024-11-15 20:39:06 +01:00
MartinOscar
24eb52f7d6 Merge pull request #709 from pelican-dev/charles/fixversio
Fix Panel Version Cache
2024-11-14 01:12:09 +01:00
notCharles
d87d3760a1 Fix Panel Version Cache 2024-11-13 19:08:46 -05:00
Boy132
fe4668a517 Update web installer (again) (#705)
* update web installer (again)

* set default values for mysql/ mariadb and redis

* add own step for queue setup

* create admin user in submit

* disable redis for queue if cache isn't redis

* remove separate user step and make session own step

* use `request()->isSecure()`
2024-11-13 18:15:48 -05:00
Lance Pioch
6125b07afa Remove old admin area (#648)
* Remove old admin

* Remove controller test

* Remove unused exceptions

* Remove unused files

* More small tweaks

* Fix doc block

* Remove unused service

* Restore these

* Add back autoDeploy

* Revert "Add back autoDeploy"

This reverts commit 630c1e08ac.

* Add these back

* Add back exception

* Remove ApiController again

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: Boy132 <mail@boy132.de>
Co-authored-by: notCharles <charles@pelican.dev>
2024-11-13 17:05:48 -05:00
Boy132
9717aa4b5f Cleanup SoftwareVersionService (#704)
* cleanup SoftwareVersionService

* fix old admin area

* show latest wings version on EditNode page

* even more cleanup
2024-11-13 16:26:10 -05:00
MartinOscar
9491322d8c Merge pull request #708 from pelican-dev/charles/fixbulk
Prevent Select All on Allocations
2024-11-13 22:25:21 +01:00
notCharles
8ed6bb4d8b pint 2024-11-13 16:22:12 -05:00
notCharles
a787af7a06 Prevent Select All
Prevent Select all on allocations, prevent people from trying to delete 30,000 ports at once ....
2024-11-13 16:21:27 -05:00
MartinOscar
d9016702d6 Merge pull request #707 from pelican-dev/charles/fixnode
Change 'exception'
2024-11-13 22:07:45 +01:00
notCharles
d565441b6a Change 'exception'
Remove the exception and just report the whole error.
2024-11-13 15:58:20 -05:00
Michael (Parker) Parker
cb522b24ef Merge pull request #706 from parkervcp/update/egg_version
use correct case for import
2024-11-09 13:59:38 -05:00
Michael (Parker) Parker
b85b17f080 use correct case for import
use lower case `v` instead of upper case `V`
2024-11-09 13:53:50 -05:00
Lance Pioch
47bd7289b1 Clear webhook cache when webhooks are deleted (#695)
* Clear webhook cache when webhooks are deleted

* fix: type casts

---------

Co-authored-by: Vehikl <go@vehikl.com>
2024-11-07 17:26:47 -05:00
Boy132
a9b76a0f51 Improve egg import error handling (#703)
* make sure read & write are successful

* show exception message in notification
2024-11-07 17:15:47 -05:00
MartinOscar
8eebb82eba Fix AutoDeploy & KeyCreationService (#701)
* Fix AutoDeploy & KeyCreationService

* Get rid of 2nd param & unset perm
2024-11-07 17:15:41 -05:00
Boy132
b3501be6ec Refactor api key permissions (#361)
* use RESOURCE_NAME for requests

* use RESOURCE_NAME for transformers

* add permissions field to api key

* add migration for new permissions field

* update tests

* remove debug log

* set column type to "json"

* remove default attribute to fix tests

* fix default value for permissions

* fix after merge

* fix after merge

* allow to "register" custom permissions

* add "role" to default resource names

* fix after merge

* fix phpstan

* fix migrations
2024-11-06 09:09:10 +01:00
Michael (Parker) Parker
ac67656d82 Merge pull request #700 from BlockyBlockling/skip-caddy-fix
Fixing Docker Environment variable only getting checked for existence instead of value
2024-11-04 11:51:05 -05:00
BlockyBlockling
968239beb3 Update entrypoint.sh
Fixed Syntax after last change
2024-11-04 13:07:57 +01:00
BlockyBlockling
7514206186 Update entrypoint.sh
Adding :- Syntax which ensures that, if SKIP_CADDY is unset, it will be treated as an empty string, which will not match "true". This avoids potential issues with unbound variables in some shell configurations where set -u (treating unset variables as an error) is enabled.

(ChatGPT)
2024-11-04 13:07:20 +01:00
BlockyBlockling
1a8321c937 Update entrypoint.sh
Fixing that its only checking for the existence of the environment variable „SKIP_CADDY“ instead of checking for its value
2024-11-04 12:43:40 +01:00
MartinOscar
340ae8099b Fix trusted proxies settings & Move ips to config & Add ipv6 (#692)
* Fix blank proxy & Move hardcoded cloudflare ips

* Add cloudflare's ipv6

* Pull from url innstead of hardcoded

* Remove Service
2024-11-01 18:16:59 -04:00
Boy132
9d02aeb130 Replace reCAPTCHA with Turnstile (#589)
* add laravel turnstile

* add config & settings for turnstile

* publish view to center captcha

* completely replace reCAPTCHA

* update FailedCaptcha event

* add back config for domain verification

* don't set language so browser lang is used
2024-11-01 18:15:04 -04:00
Charles
cf57c28c40 Update Webhooks to match other resources (#686)
* Move these

Move List/Create to their own pages to follow the flow of the other resources.

* Move EditPage aswell

* Move Save

* Labels

* Change Edit/Delete

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-11-01 18:14:20 -04:00
Boy132
382dcb3868 Fix redis connection check (#698) 2024-11-01 18:10:36 +01:00
Boy132
f793b49a81 Add egg filter to server mounts list (#697) 2024-11-01 18:10:24 +01:00
Lance Pioch
41ddae1ba0 Update ci.yaml (#643) 2024-10-31 05:39:42 -04:00
MartinOscar
e717e20996 Merge pull request #687 from RMartinOscar/fix/HealthVersion
Fix Node Health not refreshing live & Add tooltip
2024-10-30 01:58:37 +01:00
Lance Pioch
b5145b016b Update app/Models/Node.php 2024-10-29 19:53:12 -04:00
Lance Pioch
95a8f72058 Update app/Models/Node.php 2024-10-29 19:52:51 -04:00
Lance Pioch
19548338ee Update app/Models/Node.php 2024-10-29 19:52:32 -04:00
RMartinOscar
a8356fc5d2 Polishing & throw curl error 2024-10-29 20:36:44 +00:00
Boy132
7a447b04d5 Make sure roles always use web guard name (#690) 2024-10-29 18:29:25 +01:00
RMartinOscar
45699e1614 Set refresh rate 10s & Add tooltip for unreachable node 2024-10-29 15:01:30 +00:00
RMartinOscar
cde3546889 Add poll & tooltip 2024-10-29 03:28:51 +00:00
MartinOscar
3f9c1dbc3c Add prune & event blacklist (#682)
* Add prune & event blacklist

* Pinted 3times with --dirty bruh

* Add to Settings

* Fix prune & description

* Prune Logs not Configuration
2024-10-28 18:44:32 -04:00
Charles
bc2df22d78 Add unique (#685)
Usernames have to be unique, trying to make a new user with an existing username results in a 500, this fixes it.
2024-10-28 18:23:29 -04:00
Michael (Parker) Parker
1a3dc5c743 Update Egg Export Version to PLCN_V1 (#676)
* Update Egg Export Version to PLCN_V1

resolves #675

* correct version tag

* remove trailing space
2024-10-27 18:04:21 -04:00
Charles
fdd1b3798c add whereNull (#680)
Add where null to not include allocations already assigned to a server.
2024-10-27 18:01:09 -04:00
Charles
288cbee32f Fix Docker image selection (#674)
* Fix Docker image selection

Should address issue 672

Closes #672

* Fix Docker image selection in CreateServer page

---------

Co-authored-by: MartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2024-10-27 11:22:12 -04:00
MartinOscar
a70a060350 Add Soft Deletes to webhooks config table (#670) 2024-10-27 00:42:08 -04:00
MartinOscar
590569a131 Remove duplicated spa in AdminPanelProvider (#668) 2024-10-26 23:25:21 -04:00
Charles
7acc8782bb Make description required. (#667) 2024-10-26 22:06:34 -04:00
661 changed files with 11638 additions and 25438 deletions

View File

@@ -52,11 +52,11 @@ crond -L /var/log/crond -l 5
export SUPERVISORD_CADDY=false
## disable caddy if SKIP_CADDY is set
if [[ -z $SKIP_CADDY ]]; then
if [[ "${SKIP_CADDY:-}" == "true" ]]; then
echo "Starting PHP-FPM only"
else
echo "Starting PHP-FPM and Caddy"
export SUPERVISORD_CADDY=true
else
echo "Starting PHP-FPM only"
fi
chown -R www-data:www-data /pelican-data/.env /pelican-data/database

View File

@@ -87,7 +87,7 @@ jobs:
fail-fast: false
matrix:
php: [8.2, 8.3]
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Checks;
use App\Models\Node;
use App\Services\Helpers\SoftwareVersionService;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
use Spatie\Health\Enums\Status;
class NodeVersionsCheck extends Check
{
public function __construct(private SoftwareVersionService $versionService) {}
public function run(): Result
{
$all = Node::query()->count();
if ($all === 0) {
$result = Result::make()->notificationMessage('No Nodes created')->shortSummary('No Nodes');
$result->status = Status::skipped();
return $result;
}
$latestVersion = $this->versionService->latestWingsVersion();
$outdated = Node::query()->get()
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && $node->systemInformation()['version'] !== $latestVersion)
->count();
$result = Result::make()
->meta([
'all' => $all,
'outdated' => $outdated,
])
->shortSummary($outdated === 0 ? 'All up-to-date' : "{$outdated}/{$all} outdated");
return $outdated === 0
? $result->ok('All Nodes are up-to-date.')
: $result->failed(':outdated/:all Nodes are outdated.');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Checks;
use App\Services\Helpers\SoftwareVersionService;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
class PanelVersionCheck extends Check
{
public function __construct(private SoftwareVersionService $versionService) {}
public function run(): Result
{
$isLatest = $this->versionService->isLatestPanel();
$currentVersion = $this->versionService->currentPanelVersion();
$latestVersion = $this->versionService->latestPanelVersion();
$result = Result::make()
->meta([
'isLatest' => $isLatest,
'currentVersion' => $currentVersion,
'latestVersion' => $latestVersion,
])
->shortSummary($isLatest ? 'up-to-date' : 'outdated');
return $isLatest
? $result->ok('Panel is up-to-date.')
: $result->failed('Installed version is `:currentVersion` but latest is `:latestVersion`.');
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Checks;
use Spatie\Health\Checks\Checks\UsedDiskSpaceCheck as BaseCheck;
class UsedDiskSpaceCheck extends BaseCheck
{
protected function getDiskUsagePercentage(): int
{
$freeSpace = disk_free_space($this->filesystemName ?? '/');
$totalSpace = disk_total_space($this->filesystemName ?? '/');
return 100 - ($freeSpace * 100 / $totalSpace);
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\Helpers\SoftwareVersionService;
class InfoCommand extends Command
{
@@ -11,98 +10,8 @@ class InfoCommand extends Command
protected $signature = 'p:info';
/**
* InfoCommand constructor.
*/
public function __construct(private SoftwareVersionService $versionService)
{
parent::__construct();
}
/**
* Handle execution of command.
*/
public function handle(): void
{
$this->output->title('Version Information');
$this->table([], [
['Panel Version', $this->versionService->versionData()['version']],
['Latest Version', $this->versionService->getPanel()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
], 'compact');
$this->output->title('Application Configuration');
$this->table([], [
['Environment', config('app.env') === 'production' ? config('app.env') : $this->formatText(config('app.env'), 'bg=red')],
['Debug Mode', config('app.debug') ? $this->formatText('Yes', 'bg=red') : 'No'],
['Application Name', config('app.name')],
['Application URL', config('app.url')],
['Installation Directory', base_path()],
['Cache Driver', config('cache.default')],
['Queue Driver', config('queue.default') === 'sync' ? $this->formatText(config('queue.default'), 'bg=red') : config('queue.default')],
['Session Driver', config('session.driver')],
['Filesystem Driver', config('filesystems.default')],
], 'compact');
$this->output->title('Database Configuration');
$driver = config('database.default');
if ($driver === 'sqlite') {
$this->table([], [
['Driver', $driver],
['Database', config("database.connections.$driver.database")],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
['Host', config("database.connections.$driver.host")],
['Port', config("database.connections.$driver.port")],
['Database', config("database.connections.$driver.database")],
['Username', config("database.connections.$driver.username")],
], 'compact');
}
$this->output->title('Email Configuration');
$driver = config('mail.default');
if ($driver === 'smtp') {
$this->table([], [
['Driver', $driver],
['Host', config("mail.mailers.$driver.host")],
['Port', config("mail.mailers.$driver.port")],
['Username', config("mail.mailers.$driver.username")],
['Encryption', config("mail.mailers.$driver.encryption")],
['From Address', config('mail.from.address')],
['From Name', config('mail.from.name')],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
['From Address', config('mail.from.address')],
['From Name', config('mail.from.name')],
], 'compact');
}
$this->output->title('Backup Configuration');
$driver = config('backups.default');
if ($driver === 's3') {
$this->table([], [
['Driver', $driver],
['Region', config("backups.disks.$driver.region")],
['Bucket', config("backups.disks.$driver.bucket")],
['Endpoint', config("backups.disks.$driver.endpoint")],
['Use path style endpoint', config("backups.disks.$driver.use_path_style_endpoint") ? 'Yes' : 'No'],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
], 'compact');
}
}
/**
* Format output in a Name: Value manner.
*/
private function formatText(string $value, string $opts = ''): string
{
return sprintf('<%s>%s</>', $opts, $value);
$this->call('about');
}
}

View File

@@ -65,6 +65,7 @@ class BulkPowerActionCommand extends Command
$bar = $this->output->createProgressBar($count);
$powerRepository = $this->powerRepository;
// @phpstan-ignore-next-line
$this->getQueryBuilder($servers, $nodes)->each(function (Server $server) use ($action, $powerRepository, &$bar) {
$bar->clear();

View File

@@ -9,9 +9,12 @@ use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog;
use App\Models\Webhook;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Spatie\Health\Commands\RunHealthChecksCommand;
use Spatie\Health\Commands\ScheduleCheckHeartbeatCommand;
class Kernel extends ConsoleKernel
{
@@ -48,5 +51,12 @@ class Kernel extends ConsoleKernel
if (config('activity.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily();
}
if (config('panel.webhook.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [Webhook::class]])->daily();
}
$schedule->command(ScheduleCheckHeartbeatCommand::class)->everyMinute();
$schedule->command(RunHealthChecksCommand::class)->everyFiveMinutes();
}
}

View File

@@ -52,4 +52,45 @@ enum ContainerStatus: string
self::Offline => 'gray',
};
}
public function colorHex(): string
{
return match ($this) {
self::Created, self::Restarting => '#2563EB',
self::Starting, self::Paused, self::Removing, self::Stopping => '#D97706',
self::Running => '#22C55E',
self::Exited, self::Missing, self::Dead, self::Offline => '#EF4444',
};
}
public function isStartingOrStopping(): bool
{
return in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
}
public function isStartable(): bool
{
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
}
public function isRestartable(): bool
{
if ($this->isStartable()) {
return true;
}
return !in_array($this, [ContainerStatus::Offline]);
}
public function isStoppable(): bool
{
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]);
}
public function isKillable(): bool
{
// [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created]
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
enum EditorLanguages: string implements HasLabel
{
case plaintext = 'plaintext';
case abap = 'abap';
case apex = 'apex';
case azcali = 'azcali';
case bat = 'bat';
case bicep = 'bicep';
case cameligo = 'cameligo';
case coljure = 'coljure';
case coffeescript = 'coffeescript';
case c = 'c';
case cpp = 'cpp';
case csharp = 'csharp';
case csp = 'csp';
case css = 'css';
case cypher = 'cypher';
case dart = 'dart';
case dockerfile = 'dockerfile';
case ecl = 'ecl';
case elixir = 'elixir';
case flow9 = 'flow9';
case fsharp = 'fsharp';
case go = 'go';
case graphql = 'graphql';
case handlebars = 'handlebars';
case hcl = 'hcl';
case html = 'html';
case ini = 'ini';
case java = 'java';
case javascript = 'javascript';
case julia = 'julia';
case kotlin = 'kotlin';
case less = 'less';
case lexon = 'lexon';
case lua = 'lua';
case liquid = 'liquid';
case m3 = 'm3';
case markdown = 'markdown';
case mdx = 'mdx';
case mips = 'mips';
case msdax = 'msdax';
case mysql = 'mysql';
case objectivec = 'objective-c';
case pascal = 'pascal';
case pascaligo = 'pascaligo';
case perl = 'perl';
case pgsql = 'pgsql';
case php = 'php';
case pla = 'pla';
case postiats = 'postiats';
case powerquery = 'powerquery';
case powershell = 'powershell';
case proto = 'proto';
case pug = 'pug';
case python = 'python';
case qsharp = 'qsharp';
case r = 'r';
case razor = 'razor';
case redis = 'redis';
case redshift = 'redshift';
case restructuredtext = 'restructuredtext';
case ruby = 'ruby';
case rust = 'rust';
case sb = 'sb';
case scala = 'scala';
case scheme = 'scheme';
case scss = 'scss';
case shell = 'shell';
case sol = 'sol';
case aes = 'aes';
case sparql = 'sparql';
case sql = 'sql';
case st = 'st';
case swift = 'swift';
case systemverilog = 'systemverilog';
case verilog = 'verilog';
case tcl = 'tcl';
case twig = 'twig';
case typescript = 'typescript';
case typespec = 'typespec';
case vb = 'vb';
case wgsl = 'wgsl';
case xml = 'xml';
case yaml = 'yaml';
case json = 'json';
public function getLabel(): ?string
{
return $this->name;
}
}

View File

@@ -13,4 +13,5 @@ enum RolePermissionModels: string
case Role = 'role';
case Server = 'server';
case User = 'user';
case Webhook = 'webhook';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum ServerResourceType
{
case Unit;
case Percentage;
case Time;
}

View File

@@ -8,9 +8,7 @@ use Illuminate\Database\Eloquent\Model;
class ActivityLogged extends Event
{
public function __construct(public ActivityLog $model)
{
}
public function __construct(public ActivityLog $model) {}
public function is(string $event): bool
{

View File

@@ -7,7 +7,5 @@ use App\Events\Event;
class DirectLogin extends Event
{
public function __construct(public User $user, public bool $remember)
{
}
public function __construct(public User $user, public bool $remember) {}
}

View File

@@ -12,7 +12,5 @@ class FailedCaptcha extends Event
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public string $domain)
{
}
public function __construct(public string $ip, public ?string $message) {}
}

View File

@@ -12,7 +12,5 @@ class FailedPasswordReset extends Event
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public string $email)
{
}
public function __construct(public string $ip, public string $email) {}
}

View File

@@ -7,7 +7,5 @@ use App\Events\Event;
class ProvidedAuthenticationToken extends Event
{
public function __construct(public User $user, public bool $recovery = false)
{
}
public function __construct(public User $user, public bool $recovery = false) {}
}

View File

@@ -2,6 +2,4 @@
namespace App\Events;
abstract class Event
{
}
abstract class Event {}

View File

@@ -13,7 +13,5 @@ class Installed extends Event
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
public function __construct(public Server $server, public bool $successful, public bool $initialInstall) {}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class SubUserAdded extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser) {}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use App\Models\User;
use Illuminate\Queue\SerializesModels;
class SubUserRemoved extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server, public User $user) {}
}

View File

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

View File

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

View File

@@ -10,7 +10,6 @@ use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Container\Container;
use Prologue\Alerts\AlertsMessageBag;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class DisplayException extends PanelException implements HttpExceptionInterface
@@ -67,9 +66,6 @@ class DisplayException extends PanelException implements HttpExceptionInterface
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();
}

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Http\Base;
use App\Exceptions\DisplayException;
class InvalidPasswordProvidedException extends DisplayException
{
}
class InvalidPasswordProvidedException extends DisplayException {}

View File

@@ -2,14 +2,11 @@
namespace App\Exceptions\Http\Connection;
use Exception;
use Illuminate\Http\Response;
use GuzzleHttp\Exception\GuzzleException;
use App\Exceptions\DisplayException;
use Illuminate\Support\Facades\Context;
/**
* @method \GuzzleHttp\Exception\GuzzleException getPrevious()
*/
class DaemonConnectionException extends DisplayException
{
private int $statusCode = Response::HTTP_GATEWAY_TIMEOUT;
@@ -25,7 +22,7 @@ class DaemonConnectionException extends DisplayException
/**
* Throw a displayable exception caused by a daemon connection error.
*/
public function __construct(GuzzleException $previous, bool $useStatusCode = true)
public function __construct(?Exception $previous, bool $useStatusCode = true)
{
/** @var \GuzzleHttp\Psr7\Response|null $response */
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Http\Server;
use App\Exceptions\DisplayException;
class FileTypeNotEditableException extends DisplayException
{
}

View File

@@ -2,6 +2,4 @@
namespace App\Exceptions;
class PanelException extends \Exception
{
}
class PanelException extends \Exception {}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Repository\Daemon;
use App\Exceptions\Repository\RepositoryException;
class InvalidPowerSignalException extends RepositoryException
{
}

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Repository;
use App\Exceptions\DisplayException;
class DuplicateDatabaseNameException extends DisplayException
{
}
class DuplicateDatabaseNameException extends DisplayException {}

View File

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

View File

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

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class ServerUsingAllocationException extends DisplayException
{
}
class ServerUsingAllocationException extends DisplayException {}

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableAllocationException extends DisplayException
{
}
class NoViableAllocationException extends DisplayException {}

View File

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

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service\Egg;
use App\Exceptions\DisplayException;
class HasChildrenException extends DisplayException
{
}
class HasChildrenException extends DisplayException {}

View File

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

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service\Egg\Variable;
use App\Exceptions\DisplayException;
class BadValidationRuleException extends DisplayException
{
}
class BadValidationRuleException extends DisplayException {}

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service\Egg\Variable;
use App\Exceptions\DisplayException;
class ReservedVariableNameException extends DisplayException
{
}
class ReservedVariableNameException extends DisplayException {}

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service;
use App\Exceptions\DisplayException;
class InvalidFileUploadException extends DisplayException
{
}
class InvalidFileUploadException extends DisplayException {}

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service\Node;
use App\Exceptions\DisplayException;
class ConfigurationNotPersistedException extends DisplayException
{
}
class ConfigurationNotPersistedException extends DisplayException {}

View File

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

View File

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

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service\Subuser;
use App\Exceptions\DisplayException;
class ServerSubuserExistsException extends DisplayException
{
}
class ServerSubuserExistsException extends DisplayException {}

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions\Service\Subuser;
use App\Exceptions\DisplayException;
class UserIsServerOwnerException extends DisplayException
{
}
class UserIsServerOwnerException extends DisplayException {}

View File

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

View File

@@ -27,9 +27,7 @@ class BackupManager
/**
* BackupManager constructor.
*/
public function __construct(protected Application $app)
{
}
public function __construct(protected Application $app) {}
/**
* Returns a backup adapter instance.

View File

@@ -1,13 +0,0 @@
<?php
namespace App\Extensions\Facades;
use Illuminate\Support\Facades\Facade;
class Theme extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'extensions.themes';
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Extensions\Themes;
class Theme
{
public function js(string $path): string
{
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
}
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(string $path): string
{
return '/themes/panel/' . ltrim($path, '/');
}
}

View File

@@ -1,14 +0,0 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
use App\Services\Activity\ActivityLogBatchService;
class LogBatch extends Facade
{
protected static function getFacadeAccessor(): string
{
return ActivityLogBatchService::class;
}
}

View File

@@ -1,8 +1,9 @@
<?php
namespace App\Filament\Pages;
namespace App\Filament\Admin\Pages;
use App\Filament\Resources\NodeResource\Pages\ListNodes;
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
use App\Filament\Admin\Resources\NodeResource\Pages\ListNodes;
use App\Models\Egg;
use App\Models\Node;
use App\Models\Server;
@@ -39,8 +40,8 @@ class Dashboard extends Page
{
return [
'inDevelopment' => config('app.version') === 'canary',
'version' => $this->softwareVersionService->versionData()['version'],
'latestVersion' => $this->softwareVersionService->getPanel(),
'version' => $this->softwareVersionService->currentPanelVersion(),
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
'isLatest' => $this->softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
@@ -65,13 +66,13 @@ class Dashboard extends Page
CreateAction::make()
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
->icon('tabler-server-2')
->url(route('filament.admin.resources.nodes.create')),
->url(CreateNode::getUrl()),
],
'supportActions' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url($this->softwareVersionService->getDonations(), true)
->url('https://pelican.dev/donate', true)
->color('success'),
],
'helpActions' => [

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Filament\Admin\Pages;
use Carbon\Carbon;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Artisan;
use Spatie\Health\Commands\RunHealthChecksCommand;
use Spatie\Health\ResultStores\ResultStore;
class Health extends Page
{
protected static ?string $navigationIcon = 'tabler-heart';
protected static ?string $navigationGroup = 'Advanced';
protected static string $view = 'filament.pages.health';
// @phpstan-ignore-next-line
protected $listeners = [
'refresh-component' => '$refresh',
];
protected function getActions(): array
{
return [
Action::make('refresh')
->button()
->action('refresh'),
];
}
protected function getViewData(): array
{
// @phpstan-ignore-next-line
$checkResults = app(ResultStore::class)->latestResults();
if ($checkResults === null) {
Artisan::call(RunHealthChecksCommand::class);
$this->dispatch('refresh-component');
}
return [
'lastRanAt' => new Carbon($checkResults?->finishedAt),
'checkResults' => $checkResults,
];
}
public function refresh(): void
{
Artisan::call(RunHealthChecksCommand::class);
$this->dispatch('refresh-component');
Notification::make()
->title('Health check results refreshed')
->success()
->send();
}
public static function getNavigationBadge(): ?string
{
// @phpstan-ignore-next-line
$results = app(ResultStore::class)->latestResults();
if ($results === null) {
return null;
}
$results = json_decode($results->toJson(), true);
$failed = array_reduce($results['checkResults'], function ($numFailed, $result) {
return $numFailed + ($result['status'] === 'failed' ? 1 : 0);
}, 0);
return $failed === 0 ? null : (string) $failed;
}
public static function getNavigationBadgeColor(): string
{
return self::getNavigationBadge() > null ? 'danger' : '';
}
public static function getNavigationBadgeTooltip(): ?string
{
// @phpstan-ignore-next-line
$results = app(ResultStore::class)->latestResults();
if ($results === null) {
return null;
}
$results = json_decode($results->toJson(), true);
$failedNames = array_reduce($results['checkResults'], function ($carry, $result) {
if ($result['status'] === 'failed') {
$carry[] = $result['name'];
}
return $carry;
}, []);
return 'Failed: ' . implode(', ', $failedNames);
}
public static function getNavigationIcon(): string
{
// @phpstan-ignore-next-line
$results = app(ResultStore::class)->latestResults();
if ($results === null) {
return 'tabler-heart-question';
}
return $results->containsFailingCheck() ? 'tabler-heart-exclamation' : 'tabler-heart-check';
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Pages;
namespace App\Filament\Admin\Pages;
use App\Models\Backup;
use App\Notifications\MailTested;
@@ -8,7 +8,9 @@ use App\Traits\EnvironmentWriterTrait;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
@@ -21,11 +23,14 @@ use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page;
use Filament\Support\Enums\MaxWidth;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\HtmlString;
/**
* @property Form $form
@@ -33,7 +38,6 @@ use Illuminate\Support\Facades\Notification as MailNotification;
class Settings extends Page implements HasForms
{
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;
use InteractsWithHeaderActions;
@@ -67,10 +71,11 @@ class Settings extends Page implements HasForms
->label('General')
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('recaptcha')
->label('reCAPTCHA')
Tab::make('captcha')
->label('Captcha')
->icon('tabler-shield')
->schema($this->recaptchaSettings()),
->schema($this->captchaSettings())
->columns(3),
Tab::make('mail')
->label('Mail')
->icon('tabler-mail')
@@ -79,6 +84,10 @@ class Settings extends Page implements HasForms
->label('Backup')
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('OAuth')
->label('OAuth')
->icon('tabler-brand-oauth')
->schema($this->oauthSettings()),
Tab::make('misc')
->label('Misc')
->icon('tabler-tool')
@@ -146,7 +155,7 @@ class Settings extends Page implements HasForms
->separator()
->splitKeys(['Tab', ' '])
->placeholder('New IP or IP Range')
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
->hintActions([
FormAction::make('clear')
->label('Clear')
@@ -159,56 +168,77 @@ class Settings extends Page implements HasForms
->label('Set to Cloudflare IPs')
->icon('tabler-brand-cloudflare')
->authorize(fn () => auth()->user()->can('update settings'))
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'141.101.64.0/18',
'108.162.192.0/18',
'190.93.240.0/20',
'188.114.96.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'162.158.0.0/15',
'104.16.0.0/13',
'104.24.0.0/14',
'172.64.0.0/13',
'131.0.72.0/22',
])),
->action(function (Factory $client, Set $set) {
$ips = collect();
try {
$response = $client
->timeout(3)
->connectTimeout(3)
->get('https://api.cloudflare.com/client/v4/ips');
if ($response->getStatusCode() === 200) {
$result = $response->json('result');
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
$ips->push(...data_get($result, $value));
}
$ips->unique();
}
} catch (Exception) {
}
$set('TRUSTED_PROXIES', $ips->values()->all());
}),
]),
Select::make('FILAMENT_WIDTH')
->label('Display Width')
->native(false)
->options(MaxWidth::class)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
];
}
private function recaptchaSettings(): array
private function captchaSettings(): array
{
return [
Toggle::make('RECAPTCHA_ENABLED')
->label('Enable reCAPTCHA?')
Toggle::make('TURNSTILE_ENABLED')
->label('Enable Turnstile Captcha?')
->inline(false)
->columnSpan(1)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
TextInput::make('RECAPTCHA_DOMAIN')
->label('Domain')
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
Placeholder::make('info')
->columnSpan(2)
->content(new HtmlString('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
TextInput::make('TURNSTILE_SITE_KEY')
->label('Site Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
TextInput::make('RECAPTCHA_WEBSITE_KEY')
->label('Website Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
TextInput::make('RECAPTCHA_SECRET_KEY')
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
->placeholder('1x00000000000000000000AA'),
TextInput::make('TURNSTILE_SECRET_KEY')
->label('Secret Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key')))
->placeholder('1x0000000000000000000000000000000AA'),
Toggle::make('TURNSTILE_VERIFY_DOMAIN')
->label('Verify domain?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state))
->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))),
];
}
@@ -220,12 +250,12 @@ class Settings extends Page implements HasForms
->columnSpanFull()
->inline()
->options([
'log' => 'Print mails to Log',
'log' => '/storage/logs Directory',
'smtp' => 'SMTP Server',
'sendmail' => 'sendmail Binary',
'mailgun' => 'Mailgun',
'mandrill' => 'Mandrill',
'postmark' => 'Postmark',
'sendmail' => 'sendmail (PHP)',
])
->live()
->default(env('MAIL_MAILER', config('mail.default')))
@@ -386,6 +416,74 @@ class Settings extends Page implements HasForms
];
}
private function oauthSettings(): array
{
$oauthProviders = Config::get('auth.oauth');
$formFields = [];
foreach ($oauthProviders as $providerName => $providerConfig) {
$providerEnvPrefix = strtoupper($providerName);
$fields = [
Toggle::make("OAUTH_{$providerEnvPrefix}_ENABLED")
->onColor('success')
->offColor('danger')
->onIcon('tabler-check')
->offIcon('tabler-x')
->live()
->columnSpan(1)
->label('Enabled')
->default(env("OAUTH_{$providerEnvPrefix}_ENABLED", false)),
];
if (array_key_exists('client_id', $providerConfig['service'] ?? [])) {
$fields[] = TextInput::make("OAUTH_{$providerEnvPrefix}_CLIENT_ID")
->label('Client ID')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->hidden(fn (Get $get) => !$get("OAUTH_{$providerEnvPrefix}_ENABLED"))
->default(env("OAUTH_{$providerEnvPrefix}_CLIENT_ID", $providerConfig['service']['client_id'] ?? ''))
->placeholder('Client ID');
}
if (array_key_exists('client_secret', $providerConfig['service'] ?? [])) {
$fields[] = TextInput::make("OAUTH_{$providerEnvPrefix}_CLIENT_SECRET")
->label('Client Secret')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->hidden(fn (Get $get) => !$get("OAUTH_{$providerEnvPrefix}_ENABLED"))
->default(env("OAUTH_{$providerEnvPrefix}_CLIENT_SECRET", $providerConfig['service']['client_secret'] ?? ''))
->placeholder('Client Secret');
}
if (array_key_exists('base_url', $providerConfig['service'] ?? [])) {
$fields[] = TextInput::make("OAUTH_{$providerEnvPrefix}_BASE_URL")
->label('Base URL')
->columnSpanFull()
->autocomplete(false)
->hidden(fn (Get $get) => !$get("OAUTH_{$providerEnvPrefix}_ENABLED"))
->default(env("OAUTH_{$providerEnvPrefix}_BASE_URL", ''))
->placeholder('Base URL');
}
$formFields[] = Section::make(ucfirst($providerName))
->columns(5)
->icon($providerConfig['icon'] ?? 'tabler-brand-oauth')
->collapsed(fn () => !env("OAUTH_{$providerEnvPrefix}_ENABLED", false))
->collapsible()
->schema($fields);
}
return $formFields;
}
private function miscSettings(): array
{
return [
@@ -540,7 +638,21 @@ class Settings extends Page implements HasForms
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
]),
Section::make('Webhook')
->description('Configure how often old webhook logs should be pruned.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('APP_WEBHOOK_PRUNE_DAYS')
->label('Prune age')
->required()
->numeric()
->minValue(1)
->maxValue(365)
->suffix('Days')
->default(env('APP_WEBHOOK_PRUNE_DAYS', config('panel.webhook.prune_days'))),
]),
];
}
@@ -549,11 +661,6 @@ class Settings extends Page implements HasForms
return 'data';
}
protected function hasUnsavedDataChangesAlert(): bool
{
return true;
}
public function save(): void
{
try {
@@ -567,8 +674,6 @@ class Settings extends Page implements HasForms
Artisan::call('config:clear');
Artisan::call('queue:restart');
$this->rememberData();
$this->redirect($this->getUrl());
Notification::make()

View File

@@ -1,17 +1,20 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Filament\Resources\ApiKeyResource\Pages;
use App\Filament\Admin\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 $modelLabel = 'Application API Key';
protected static ?string $pluralModelLabel = 'Application API Keys';
protected static ?string $navigationLabel = 'API Keys';
protected static ?string $navigationIcon = 'tabler-key';
@@ -22,11 +25,6 @@ class ApiKeyResource extends Resource
return static::getModel()::where('key_type', ApiKey::TYPE_APPLICATION)->count() ?: null;
}
public static function canEdit(Model $record): bool
{
return false;
}
public static function getPages(): array
{
return [

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\ApiKeyResource\Pages;
namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Resources\ApiKeyResource;
use App\Filament\Admin\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;
@@ -11,12 +11,25 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateApiKey extends CreateRecord
{
protected static string $resource = ApiKeyResource::class;
protected ?string $heading = 'Create Application API Key';
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
public function form(Form $form): Form
{
@@ -41,7 +54,7 @@ class CreateApiKey extends CreateRecord
'md' => 2,
])
->schema(
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
->label(str($resource)->replace('_', ' ')->title())->inline()
->options([
0 => 'None',
@@ -87,4 +100,20 @@ class CreateApiKey extends CreateRecord
->columnSpanFull(),
]);
}
protected function handleRecordCreation(array $data): Model
{
$permissions = [];
foreach (ApiKey::getPermissionList() as $permission) {
if (isset($data['permissions_' . $permission])) {
$permissions[$permission] = intval($data['permissions_' . $permission]);
unset($data['permissions_' . $permission]);
}
}
$data['permissions'] = $permissions;
return parent::handleRecordCreation($data);
}
}

View File

@@ -1,8 +1,9 @@
<?php
namespace App\Filament\Resources\ApiKeyResource\Pages;
namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Resources\ApiKeyResource;
use App\Filament\Admin\Resources\ApiKeyResource;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Models\ApiKey;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@@ -35,15 +36,13 @@ class ListApiKeys extends ListRecords
->hidden()
->searchable(),
TextColumn::make('last_used_at')
DateTimeColumn::make('last_used_at')
->label('Last Used')
->placeholder('Not Used')
->dateTime()
->sortable(),
TextColumn::make('created_at')
DateTimeColumn::make('created_at')
->label('Created')
->dateTime()
->sortable(),
TextColumn::make('user.username')

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Models\DatabaseHost;
use Filament\Resources\Resource;
@@ -10,12 +10,12 @@ class DatabaseHostResource extends Resource
{
protected static ?string $model = DatabaseHost::class;
protected static ?string $label = 'Database Host';
protected static ?string $navigationIcon = 'tabler-database';
protected static ?string $navigationGroup = 'Advanced';
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;

View File

@@ -1,11 +1,9 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\Pages;
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService;
use Closure;
use Exception;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
@@ -13,20 +11,17 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Exceptions\Halt;
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';
protected static bool $canCreateAnother = false;
protected ?string $subheading = '(database servers that can have individual databases)';
private HostCreationService $service;
public function boot(HostCreationService $service): void
{
@@ -79,12 +74,13 @@ class CreateDatabaseHost extends CreateRecord
->revealable()
->maxLength(255)
->required(),
Select::make('node_id')
Select::make('node_ids')
->multiple()
->searchable()
->preload()
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
->label('Linked Node')
->relationship('node', 'name'),
->label('Linked Nodes')
->relationship('nodes', 'name'),
]),
]);
}
@@ -103,21 +99,18 @@ class CreateDatabaseHost extends CreateRecord
protected function handleRecordCreation(array $data): Model
{
return $this->service->handle($data);
}
public function exception(Exception $e, Closure $stopPropagation): void
{
if ($e instanceof PDOException) {
try {
return $this->service->handle($data);
} catch (PDOException $exception) {
Notification::make()
->title('Error connecting to database host')
->body($e->getMessage())
->body($exception->getMessage())
->color('danger')
->icon('tabler-database')
->danger()
->send();
$stopPropagation();
throw new Halt();
}
}
}

View File

@@ -1,13 +1,11 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\Pages;
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
use App\Models\DatabaseHost;
use App\Services\Databases\Hosts\HostUpdateService;
use Closure;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Section;
@@ -16,6 +14,7 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Model;
use PDOException;
@@ -74,12 +73,13 @@ class EditDatabaseHost extends EditRecord
->password()
->revealable()
->maxLength(255),
Select::make('node_id')
Select::make('nodes')
->multiple()
->searchable()
->preload()
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
->label('Linked Node')
->relationship('node', 'name'),
->label('Linked Nodes')
->relationship('nodes', 'name'),
]),
]);
}
@@ -101,9 +101,13 @@ class EditDatabaseHost extends EditRecord
public function getRelationManagers(): array
{
return [
DatabasesRelationManager::class,
];
if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) {
return [
DatabasesRelationManager::class,
];
}
return [];
}
protected function handleRecordUpdate(Model $record, array $data): Model
@@ -112,21 +116,18 @@ class EditDatabaseHost extends EditRecord
return $record;
}
return $this->hostUpdateService->handle($record, $data);
}
public function exception(Exception $e, Closure $stopPropagation): void
{
if ($e instanceof PDOException) {
try {
return $this->hostUpdateService->handle($record, $data);
} catch (PDOException $exception) {
Notification::make()
->title('Error connecting to database host')
->body($e->getMessage())
->body($exception->getMessage())
->color('danger')
->icon('tabler-database')
->danger()
->send();
$stopPropagation();
throw new Halt();
}
}
}

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\Pages;
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Models\DatabaseHost;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@@ -36,8 +36,9 @@ class ListDatabaseHosts extends ListRecords
->counts('databases')
->icon('tabler-database')
->label('Databases'),
TextColumn::make('node.name')
TextColumn::make('nodes.name')
->icon('tabler-server-2')
->badge()
->placeholder('No Nodes')
->sortable(),
])

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Models\Database;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class DatabasesRelationManager extends RelationManager
{
protected static string $relationship = 'databases';
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('database')
->columnSpanFull(),
TextInput::make('username'),
TextInput::make('password')
->password()
->revealable()
->hintAction(RotateDatabasePasswordAction::make())
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->label('Connections From')
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote),
TextInput::make('max_connections')
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections),
TextInput::make('jdbc')
->label('JDBC Connection String')
->columnSpanFull()
->password()
->revealable()
->formatStateUsing(fn (Database $database) => $database->jdbc),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('servers')
->columns([
TextColumn::make('database')
->icon('tabler-database'),
TextColumn::make('username')
->icon('tabler-user'),
TextColumn::make('remote')
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote),
TextColumn::make('server.name')
->icon('tabler-brand-docker')
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
TextColumn::make('max_connections')
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections),
DateTimeColumn::make('created_at'),
])
->actions([
DeleteAction::make()
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
ViewAction::make()
->color('primary')
->hidden(fn () => !auth()->user()->can('viewList database')),
]);
}
}

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Filament\Resources\EggResource\Pages;
use App\Filament\Admin\Resources\EggResource\Pages;
use App\Models\Egg;
use Filament\Resources\Resource;
@@ -14,8 +14,6 @@ class EggResource extends Resource
protected static ?string $recordTitleAttribute = 'name';
protected static ?string $recordRouteKeyName = 'id';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;

View File

@@ -1,9 +1,9 @@
<?php
namespace App\Filament\Resources\EggResource\Pages;
namespace App\Filament\Admin\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Resources\EggResource;
use App\Filament\Admin\Resources\EggResource;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;
@@ -28,6 +28,18 @@ class CreateEgg extends CreateRecord
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
public function form(Form $form): Form
{
return $form

View File

@@ -1,21 +1,18 @@
<?php
namespace App\Filament\Resources\EggResource\Pages;
namespace App\Filament\Admin\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Resources\EggResource;
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
use App\Filament\Admin\Resources\EggResource;
use App\Filament\Admin\Resources\EggResource\RelationManagers\ServersRelationManager;
use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
@@ -26,7 +23,6 @@ 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;
class EditEgg extends EditRecord
@@ -229,6 +225,7 @@ class EditEgg extends EditRecord
->default('ash'),
MonacoEditor::make('script_install')
->label('Install Script')
->placeholderText('')
->columnSpanFull()
->fontSize('16px')
->language('shell')
@@ -241,83 +238,11 @@ class EditEgg extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make('deleteEgg')
DeleteAction::make()
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
Actions\Action::make('exportEgg')
->label('Export')
->color('primary')
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json'))
->authorize(fn () => auth()->user()->can('export egg')),
Actions\Action::make('importEgg')
->label('Import')
->form([
Placeholder::make('warning')
->label('This will overwrite the current egg to the one you upload.'),
Tabs::make('Tabs')
->tabs([
Tab::make('From File')
->icon('tabler-file-upload')
->schema([
FileUpload::make('egg')
->label('Egg')
->hint('eg. minecraft.json')
->acceptedFileTypes(['application/json'])
->storeFiles(false),
]),
Tab::make('From URL')
->icon('tabler-world-upload')
->schema([
TextInput::make('url')
->label('URL')
->default(fn (Egg $egg): ?string => $egg->update_url)
->hint('Link to the egg file (eg. minecraft.json)')
->url(),
]),
])
->contained(false),
])
->action(function (array $data, Egg $egg, EggImporterService $eggImportService): void {
if (!empty($data['egg'])) {
try {
$eggImportService->fromFile($data['egg'], $egg);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger() // Will Robinson
->send();
report($exception);
return;
}
} elseif (!empty($data['url'])) {
try {
$eggImportService->fromUrl($data['url'], $egg);
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->send();
report($exception);
return;
}
}
$this->refreshForm();
Notification::make()
->title('Import Success')
->success()
->send();
})
->authorize(fn () => auth()->user()->can('import egg')),
ExportEggAction::make(),
ImportEggAction::make(),
$this->getSaveFormAction()->formId('form'),
];
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Filament\Admin\Resources\EggResource\Pages;
use App\Filament\Admin\Resources\EggResource;
use App\Filament\Components\Actions\ImportEggAction as ImportEggHeaderAction;
use App\Filament\Components\Tables\Actions\ExportEggAction;
use App\Filament\Components\Tables\Actions\ImportEggAction;
use App\Filament\Components\Tables\Actions\UpdateEggAction;
use App\Models\Egg;
use Filament\Actions\CreateAction as CreateHeaderAction;
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;
use Filament\Tables\Table;
class ListEggs extends ListRecords
{
protected static string $resource = EggResource::class;
public function table(Table $table): Table
{
return $table
->searchable(true)
->defaultPaginationPageOption(25)
->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0)
->columns([
TextColumn::make('id')
->label('Id')
->hidden(),
TextColumn::make('name')
->icon('tabler-egg')
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
->wrap()
->searchable()
->sortable(),
TextColumn::make('servers_count')
->counts('servers')
->icon('tabler-server')
->label('Servers'),
])
->actions([
EditAction::make(),
ExportEggAction::make(),
UpdateEggAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete egg')),
]),
])
->emptyStateIcon('tabler-eggs')
->emptyStateDescription('')
->emptyStateHeading('No Eggs')
->emptyStateActions([
CreateAction::make()
->label('Create Egg'),
ImportEggAction::make(),
]);
}
protected function getHeaderActions(): array
{
return [
ImportEggHeaderAction::make(),
CreateHeaderAction::make()
->label('Create Egg'),
];
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Resources\EggResource\RelationManagers;
namespace App\Filament\Admin\Resources\EggResource\RelationManagers;
use App\Models\Server;
use Filament\Resources\RelationManagers\RelationManager;

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Filament\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource\Pages;
use App\Models\Mount;
use Filament\Resources\Resource;
@@ -14,6 +14,8 @@ class MountResource extends Resource
protected static ?string $navigationGroup = 'Advanced';
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\MountResource\Pages;
namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use App\Filament\Admin\Resources\MountResource;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Section;
@@ -21,6 +21,18 @@ class CreateMount extends CreateRecord
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
public function form(Form $form): Form
{
return $form

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\MountResource\Pages;
namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use App\Filament\Admin\Resources\MountResource;
use Filament\Actions;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\MountResource\Pages;
namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use App\Filament\Admin\Resources\MountResource;
use App\Models\Mount;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;

View File

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

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\NodeResource\Pages;
namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Filament\Admin\Resources\NodeResource;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
@@ -23,8 +23,6 @@ class CreateNode extends CreateRecord
protected static bool $canCreateAnother = false;
protected ?string $subheading = 'which is a machine that runs your Servers';
public function form(Forms\Form $form): Forms\Form
{
return $form

View File

@@ -1,9 +1,10 @@
<?php
namespace App\Filament\Resources\NodeResource\Pages;
namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Filament\Admin\Resources\NodeResource;
use App\Models\Node;
use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
@@ -47,7 +48,12 @@ class EditNode extends EditRecord
Tab::make('')
->label('Overview')
->icon('tabler-chart-area-line-filled')
->columns(6)
->columns([
'default' => 4,
'sm' => 2,
'md' => 4,
'lg' => 4,
])
->schema([
Fieldset::make()
->label('Node Information')
@@ -55,7 +61,7 @@ class EditNode extends EditRecord
->schema([
Placeholder::make('')
->label('Wings Version')
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? 'Unknown') . ' (Latest: ' . $versionService->latestWingsVersion() . ')'),
Placeholder::make('')
->label('CPU Threads')
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
@@ -66,8 +72,20 @@ class EditNode extends EditRecord
->label('Kernel')
->content(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? 'Unknown'),
]),
View::make('filament.components.node-cpu-chart')->columnSpan(3),
View::make('filament.components.node-memory-chart')->columnSpan(3),
View::make('filament.components.node-cpu-chart')
->columnSpan([
'default' => 4,
'sm' => 1,
'md' => 2,
'lg' => 2,
]),
View::make('filament.components.node-memory-chart')
->columnSpan([
'default' => 4,
'sm' => 1,
'md' => 2,
'lg' => 2,
]),
// TODO: Make purdy View::make('filament.components.node-storage-chart')->columnSpan(3),
]),
Tab::make('Basic Settings')

View File

@@ -1,8 +1,9 @@
<?php
namespace App\Filament\Resources\NodeResource\Pages;
namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Filament\Admin\Resources\NodeResource;
use App\Filament\Components\Tables\Columns\NodeHealthColumn;
use App\Models\Node;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@@ -27,10 +28,7 @@ class ListNodes extends ListRecords
->label('UUID')
->searchable()
->hidden(),
IconColumn::make('health')
->alignCenter()
->state(fn (Node $node) => $node)
->view('livewire.columns.version-column'),
NodeHealthColumn::make('health'),
TextColumn::make('name')
->icon('tabler-server-2')
->sortable()

View File

@@ -1,13 +1,16 @@
<?php
namespace App\Filament\Resources\NodeResource\RelationManagers;
namespace App\Filament\Admin\Resources\NodeResource\RelationManagers;
use App\Filament\Admin\Resources\ServerResource\Pages\CreateServer;
use App\Models\Allocation;
use App\Models\Node;
use App\Services\Allocations\AssignmentService;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
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;
@@ -47,15 +50,20 @@ class AllocationsRelationManager extends RelationManager
// All assigned allocations
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->paginationPageOptions(['10', '20', '50', '100', '200', '500', '1000'])
->searchable()
->selectCurrentPageOnly() //Prevent people from trying to nuke 30,000 ports at once.... -,-
->columns([
TextColumn::make('id'),
TextColumn::make('id')
->toggleable()
->toggledHiddenByDefault(),
TextColumn::make('port')
->searchable()
->label('Port'),
TextColumn::make('server.name')
->label('Server')
->icon('tabler-brand-docker')
->visibleFrom('md')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
TextInputColumn::make('ip_alias')
@@ -65,21 +73,17 @@ class AllocationsRelationManager extends RelationManager
->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())
Select::make('allocation_ip')
->options(collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label('IP Address')
->inlineLabel()
->ipv4()
->helperText("Usually your machine's public IP unless you are port forwarding.")
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->live()
->required(),
TextInput::make('allocation_alias')
->label('Alias')
@@ -97,54 +101,10 @@ class AllocationsRelationManager extends RelationManager
->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());
}
})
->disabled(fn (Get $get) => empty($get('allocation_ip')))
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
CreateServer::retrieveValidPorts($this->getOwnerRecord(), $state, $get('allocation_ip')))
)
->splitKeys(['Tab', ' ', ','])
->required(),
])

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Resources\NodeResource\RelationManagers;
namespace App\Filament\Admin\Resources\NodeResource\RelationManagers;
use App\Models\Server;
use Filament\Resources\RelationManagers\RelationManager;

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Resources\NodeResource\Widgets;
namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node;
use Carbon\Carbon;
@@ -26,8 +26,8 @@ class NodeCpuChart extends ChartWidget
$cpu = collect(cache()->get("nodes.$node->id.cpu_percent"))
->slice(-10)
->map(fn ($value, $key) => [
'cpu' => Number::format($value * $threads, maxPrecision: 2, locale: auth()->user()->language),
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
'cpu' => Number::format($value * $threads, maxPrecision: 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
])
->all();
@@ -43,6 +43,7 @@ class NodeCpuChart extends ChartWidget
],
],
'labels' => array_column($cpu, 'timestamp'),
'locale' => auth()->user()->language ?? 'en',
];
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Resources\NodeResource\Widgets;
namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node;
use Carbon\Carbon;
@@ -24,8 +24,8 @@ class NodeMemoryChart extends ChartWidget
$memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10)
->map(fn ($value, $key) => [
'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'),
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
])
->all();
@@ -41,6 +41,7 @@ class NodeMemoryChart extends ChartWidget
],
],
'labels' => array_column($memUsed, 'timestamp'),
'locale' => auth()->user()->language ?? 'en',
];
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Resources\NodeResource\Widgets;
namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node;
use Filament\Widgets\ChartWidget;

View File

@@ -1,12 +1,11 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Enums\RolePermissionModels;
use App\Enums\RolePermissionPrefixes;
use App\Filament\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource\Pages;
use App\Models\Role;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Component;
@@ -25,6 +24,8 @@ class RoleResource extends Resource
protected static ?string $navigationIcon = 'tabler-users-group';
protected static ?string $navigationGroup = 'Advanced';
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationBadge(): ?string
@@ -71,7 +72,7 @@ class RoleResource extends Resource
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
TextInput::make('guard_name')
->label('Guard Name')
->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '')
->default(Role::DEFAULT_GUARD_NAME)
->nullable()
->hidden(),
Fieldset::make('Permissions')
@@ -92,6 +93,8 @@ class RoleResource extends Resource
$icon = ('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon();
} elseif (class_exists('\App\Filament\Pages\\' . $model)) {
$icon = ('\App\Filament\Pages\\' . $model)::getNavigationIcon();
} elseif (class_exists('\App\Filament\Server\Resources\\' . $model . 'Resource')) {
$icon = ('\App\Filament\Server\Resources\\' . $model . 'Resource')::getNavigationIcon();
}
return Section::make(Str::headline(Str::plural($model)))

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\RoleResource\Pages;
namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use App\Filament\Admin\Resources\RoleResource;
use App\Models\Role;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Arr;
@@ -14,11 +14,23 @@ use Spatie\Permission\Models\Permission;
*/
class CreateRole extends CreateRecord
{
public Collection $permissions;
protected static string $resource = RoleResource::class;
protected static bool $canCreateAnother = false;
public Collection $permissions;
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
protected function mutateFormDataBeforeCreate(array $data): array
{

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\RoleResource\Pages;
namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use App\Filament\Admin\Resources\RoleResource;
use App\Models\Role;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\RoleResource\Pages;
namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use App\Filament\Admin\Resources\RoleResource;
use App\Models\Role;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Filament\Resources\ServerResource\Pages;
use App\Filament\Admin\Resources\ServerResource\Pages;
use App\Models\Server;
use Filament\Resources\Resource;

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\Node;
@@ -34,7 +34,9 @@ use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Blade;
@@ -68,9 +70,8 @@ class CreateServer extends CreateRecord
->completedIcon('tabler-check')
->columns([
'default' => 1,
'sm' => 1,
'sm' => 4,
'md' => 4,
'lg' => 6,
])
->schema([
TextInput::make('name')
@@ -87,24 +88,51 @@ class CreateServer extends CreateRecord
$set('name', $prefix . $word);
}))
->columnSpan([
'default' => 2,
'sm' => 3,
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 3,
])
->required()
->maxLength(255),
TextInput::make('external_id')
->label('External ID')
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
])
->unique()
->maxLength(255),
Select::make('node_id')
->disabledOn('edit')
->prefixIcon('tabler-server-2')
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 2,
])
->live()
->relationship('node', 'name')
->searchable()
->preload()
->afterStateUpdated(function (Set $set, $state) {
$set('allocation_id', null);
$this->node = Node::find($state);
})
->required(),
Select::make('owner_id')
->preload()
->prefixIcon('tabler-user')
->default(auth()->user()->id)
->label('Owner')
->columnSpan([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 3,
'default' => 1,
'sm' => 2,
'md' => 2,
])
->relationship('user', 'username')
->searchable(['username', 'email'])
@@ -113,6 +141,7 @@ class CreateServer extends CreateRecord
TextInput::make('username')
->alphaNum()
->required()
->minLength(3)
->maxLength(255),
TextInput::make('email')
@@ -133,36 +162,15 @@ class CreateServer extends CreateRecord
})
->required(),
Select::make('node_id')
->disabledOn('edit')
->prefixIcon('tabler-server-2')
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
->columnSpan([
'default' => 2,
'sm' => 3,
'md' => 6,
'lg' => 6,
])
->live()
->relationship('node', 'name')
->searchable()
->preload()
->afterStateUpdated(function (Set $set, $state) {
$set('allocation_id', null);
$this->node = Node::find($state);
})
->required(),
Select::make('allocation_id')
->preload()
->live()
->prefixIcon('tabler-network')
->label('Primary Allocation')
->columnSpan([
'default' => 2,
'sm' => 3,
'default' => 1,
'sm' => 2,
'md' => 2,
'lg' => 3,
])
->disabled(fn (Get $get) => $get('node_id') === null)
->searchable(['ip', 'port', 'ip_alias'])
@@ -190,87 +198,47 @@ class CreateServer extends CreateRecord
->where('node_id', $get('node_id'))
->whereNull('server_id'),
)
->createOptionForm(fn (Get $get) => [
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(),
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),
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);
->createOptionForm(function (Get $get) {
$getPage = $get;
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(),
])
return [
Select::make('allocation_ip')
->options(collect(Node::find($get('node_id'))?->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label('IP Address')
->helperText("Usually your machine's public IP unless you are port forwarding.")
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->inlineLabel()
->ipv4()
->live()
->required(),
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),
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()
->disabled(fn (Get $get) => empty($get('allocation_ip')))
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
CreateServer::retrieveValidPorts(Node::find($getPage('node_id')), $state, $get('allocation_ip')))
)
->splitKeys(['Tab', ' ', ','])
->required(),
];
})
->createOptionUsing(function (array $data, Get $get, AssignmentService $assignmentService): int {
return collect(
$assignmentService->handle(Node::find($get('node_id')), $data)
@@ -281,10 +249,9 @@ class CreateServer extends CreateRecord
Repeater::make('allocation_additional')
->label('Additional Allocations')
->columnSpan([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 3,
'default' => 1,
'sm' => 2,
'md' => 2,
])
->addActionLabel('Add Allocation')
->disabled(fn (Get $get) => $get('allocation_id') === null)
@@ -320,10 +287,9 @@ class CreateServer extends CreateRecord
->placeholder('Description')
->rows(3)
->columnSpan([
'default' => 2,
'sm' => 6,
'md' => 6,
'lg' => 6,
'default' => 1,
'sm' => 4,
'md' => 4,
])
->label('Description'),
]),
@@ -534,6 +500,37 @@ class CreateServer extends CreateRecord
'lg' => 3,
])
->schema([
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0)
->helperText('100% equals one CPU core.'),
]),
Grid::make()
->columns(4)
->columnSpanFull()
@@ -564,7 +561,6 @@ class CreateServer extends CreateRecord
->numeric()
->minValue(0),
]),
Grid::make()
->columns(4)
->columnSpanFull()
@@ -596,37 +592,6 @@ class CreateServer extends CreateRecord
->minValue(0),
]),
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->default(true)
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0)
->helperText('100% equals one CPU core.'),
]),
]),
Fieldset::make('Advanced Limits')
@@ -638,6 +603,40 @@ class CreateServer extends CreateRecord
'lg' => 3,
])
->schema([
Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion')
->default(500),
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('cpu_pinning')
->label('CPU Pinning')->inlineLabel()->inline()
->default(false)
->afterStateUpdated(fn (Set $set) => $set('threads', []))
->live()
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'warning',
])
->columnSpan(2),
TagsInput::make('threads')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => !$get('cpu_pinning'))
->label('Pinned Threads')->inlineLabel()
->required(fn (Get $get) => $get('cpu_pinning'))
->columnSpan(2)
->separator()
->splitKeys([','])
->placeholder('Add pinned thread, e.g. 0 or 2-4'),
]),
Grid::make()
->columns(4)
->columnSpanFull()
@@ -686,41 +685,6 @@ class CreateServer extends CreateRecord
->integer(),
]),
Hidden::make('io')
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion')
->default(500),
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('cpu_pinning')
->label('CPU Pinning')->inlineLabel()->inline()
->default(false)
->afterStateUpdated(fn (Set $set) => $set('threads', []))
->live()
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'warning',
])
->columnSpan(2),
TagsInput::make('threads')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => !$get('cpu_pinning'))
->label('Pinned Threads')->inlineLabel()
->required(fn (Get $get) => $get('cpu_pinning'))
->columnSpan(2)
->separator()
->splitKeys([','])
->placeholder('Add pinned thread, e.g. 0 or 2-4'),
]),
Grid::make()
->columns(4)
->columnSpanFull()
@@ -787,6 +751,7 @@ class CreateServer extends CreateRecord
->schema([
Select::make('select_image')
->label('Image Name')
->live()
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
->options(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
@@ -811,7 +776,7 @@ class CreateServer extends CreateRecord
TextInput::make('image')
->label('Image')
->debounce(500)
->required()
->afterStateUpdated(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
@@ -874,7 +839,18 @@ class CreateServer extends CreateRecord
{
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
return $this->serverCreationService->handle($data);
try {
return $this->serverCreationService->handle($data);
} catch (Exception $exception) {
Notification::make()
->title('Could not create server')
->body($exception->getMessage())
->color('danger')
->danger()
->send();
throw new Halt();
}
}
private function shouldHideComponent(Get $get, Component $component): bool
@@ -907,4 +883,88 @@ class CreateServer extends CreateRecord
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
public static function retrieveValidPorts(Node $node, array $portEntries, string $ip): array
{
$portRangeLimit = AssignmentService::PORT_RANGE_LIMIT;
$portFloor = AssignmentService::PORT_FLOOR;
$portCeil = AssignmentService::PORT_CEIL;
$ports = collect();
$existingPorts = $node
->allocations()
->where('ip', $ip)
->pluck('port')
->all();
foreach ($portEntries as $portEntry) {
$start = $end = $portEntry;
if (str_contains($portEntry, '-')) {
[$start, $end] = explode('-', $portEntry);
}
if (!is_numeric($start) || !is_numeric($end)) {
Notification::make()
->title('Invalid Port Range')
->danger()
->body("Your port range are not valid integers: $portEntry")
->send();
continue;
}
$start = (int) $start;
$end = (int) $end;
$range = $start <= $end ? range($start, $end) : range($end, $start);
if (count($range) > $portRangeLimit) {
Notification::make()
->title('Too many ports at one time!')
->danger()
->body("The current limit is $portRangeLimit number of ports at one time.")
->send();
continue;
}
foreach ($range as $i) {
// Invalid port number
if ($i <= $portFloor || $i > $portCeil) {
Notification::make()
->title('Port not in valid range')
->danger()
->body("$i is not in the valid port range between $portFloor-$portCeil")
->send();
continue;
}
// Already exists
if (in_array($i, $existingPorts)) {
Notification::make()
->title('Port already in use')
->danger()
->body("$i is already with an allocation")
->send();
continue;
}
$ports->push($i);
}
}
$uniquePorts = $ports->unique()->values();
if ($ports->count() > $uniquePorts->count()) {
$ports = $uniquePorts;
}
$sortedPorts = $ports->sort()->values();
if ($sortedPorts->all() !== $ports->all()) {
$ports = $sortedPorts;
}
return $ports->all();
}
}

View File

@@ -1,20 +1,26 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Enums\ContainerStatus;
use App\Enums\ServerState;
use App\Filament\Resources\ServerResource;
use App\Http\Controllers\Admin\ServersController;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Server\Pages\Console;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Services\Databases\DatabaseManagementService;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Eggs\EggChangerService;
use App\Services\Servers\RandomWordService;
use App\Services\Servers\ReinstallServerService;
use App\Services\Servers\ServerDeletionService;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\ToggleInstallService;
use App\Services\Servers\TransferServerService;
use Closure;
use Exception;
@@ -25,6 +31,7 @@ use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
@@ -32,6 +39,7 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Forms\Get;
@@ -153,6 +161,7 @@ class EditServer extends EditRecord
'md' => 2,
'lg' => 3,
])
->unique()
->maxLength(255),
Select::make('node_id')
->label('Node')
@@ -176,6 +185,35 @@ class EditServer extends EditRecord
'lg' => 3,
])
->schema([
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
Grid::make()
->columns(4)
->columnSpanFull()
@@ -235,36 +273,6 @@ class EditServer extends EditRecord
->numeric()
->minValue(0),
]),
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->label('CPU')->inlineLabel()->inline()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->live()
->options([
true => 'Unlimited',
false => 'Limited',
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label('CPU Limit')->inlineLabel()
->suffix('%')
->required()
->columnSpan(2)
->numeric()
->minValue(0),
]),
]),
Fieldset::make('Advanced Limits')
@@ -279,6 +287,36 @@ class EditServer extends EditRecord
->columns(4)
->columnSpanFull()
->schema([
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('cpu_pinning')
->label('CPU Pinning')->inlineLabel()->inline()
->default(false)
->afterStateUpdated(fn (Set $set) => $set('threads', []))
->formatStateUsing(fn (Get $get) => !empty($get('threads')))
->live()
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'warning',
])
->columnSpan(2),
TagsInput::make('threads')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => !$get('cpu_pinning'))
->label('Pinned Threads')->inlineLabel()
->required(fn (Get $get) => $get('cpu_pinning'))
->columnSpan(2)
->separator()
->splitKeys([','])
->placeholder('Add pinned thread, e.g. 0 or 2-4'),
]),
ToggleButtons::make('swap_support')
->live()
->label('Swap Memory')->inlineLabel()->inline()
@@ -330,37 +368,6 @@ class EditServer extends EditRecord
->helperText('The IO performance relative to other running containers')
->label('Block IO Proportion'),
Grid::make()
->columns(4)
->columnSpanFull()
->schema([
ToggleButtons::make('cpu_pinning')
->label('CPU Pinning')->inlineLabel()->inline()
->default(false)
->afterStateUpdated(fn (Set $set) => $set('threads', []))
->formatStateUsing(fn (Get $get) => !empty($get('threads')))
->live()
->options([
false => 'Disabled',
true => 'Enabled',
])
->colors([
false => 'success',
true => 'warning',
])
->columnSpan(2),
TagsInput::make('threads')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => !$get('cpu_pinning'))
->label('Pinned Threads')->inlineLabel()
->required(fn (Get $get) => $get('cpu_pinning'))
->columnSpan(2)
->separator()
->splitKeys([','])
->placeholder('Add pinned thread, e.g. 0 or 2-4'),
]),
Grid::make()
->columns(4)
->columnSpanFull()
@@ -392,16 +399,19 @@ class EditServer extends EditRecord
])
->schema([
TextInput::make('allocation_limit')
->label('Allocations')
->suffixIcon('tabler-network')
->required()
->minValue(0)
->numeric(),
TextInput::make('database_limit')
->label('Databases')
->suffixIcon('tabler-database')
->required()
->minValue(0)
->numeric(),
TextInput::make('backup_limit')
->label('Backups')
->suffixIcon('tabler-copy-check')
->required()
->minValue(0)
@@ -412,11 +422,12 @@ class EditServer extends EditRecord
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 3,
'lg' => 4,
])
->schema([
Select::make('select_image')
->label('Image Name')
->live()
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
->options(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
@@ -432,11 +443,16 @@ class EditServer extends EditRecord
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
})
->selectablePlaceholder(false)
->columnSpan(1),
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 2,
]),
TextInput::make('image')
->label('Image')
->debounce(500)
->required()
->afterStateUpdated(function ($state, Get $get, Set $set) {
$egg = Egg::query()->find($get('egg_id'));
$images = $egg->docker_images ?? [];
@@ -448,9 +464,14 @@ class EditServer extends EditRecord
}
})
->placeholder('Enter a custom Image')
->columnSpan(2),
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 3,
'lg' => 2,
]),
Forms\Components\KeyValue::make('docker_labels')
KeyValue::make('docker_labels')
->label('Container Labels')
->keyLabel('Label Name')
->valueLabel('Label Description')
@@ -467,7 +488,7 @@ class EditServer extends EditRecord
])
->schema([
Select::make('egg_id')
->disabledOn('edit')
->disabled()
->prefixIcon('tabler-egg')
->columnSpan([
'default' => 6,
@@ -478,7 +499,28 @@ class EditServer extends EditRecord
->relationship('egg', 'name')
->searchable()
->preload()
->required(),
->required()
->hintAction(
Action::make('change_egg')
->action(function (array $data, Server $server, EggChangerService $service) {
$service->handle($server, $data['egg_id'], $data['keepOldVariables']);
// Use redirect instead of fillForm to prevent server variables from duplicating
$this->redirect($this->getUrl(['record' => $server, 'tab' => '-egg-tab']), true);
})
->form(fn (Server $server) => [
Select::make('egg_id')
->label('New Egg')
->prefixIcon('tabler-egg')
->options(fn () => Egg::all()->filter(fn (Egg $egg) => $egg->id !== $server->egg->id)->mapWithKeys(fn (Egg $egg) => [$egg->id => $egg->name]))
->searchable()
->preload()
->required(),
Toggle::make('keepOldVariables')
->label('Keep old variables if possible?')
->default(true),
])
),
ToggleButtons::make('skip_scripts')
->label('Run Egg Install Script?')->inline()
@@ -506,23 +548,19 @@ class EditServer extends EditRecord
->label('Startup Command')
->required()
->columnSpan(6)
->rows(function ($state) {
return str($state)->explode("\n")->reduce(
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
0
);
}),
->autosize(),
Textarea::make('defaultStartup')
->hintAction(CopyAction::make())
->label('Default Startup Command')
->disabled()
->autosize()
->columnSpan(6)
->formatStateUsing(function ($state, Get $get) {
$egg = Egg::query()->find($get('egg_id'));
return $egg->startup;
})
->columnSpan(6),
}),
Repeater::make('server_variables')
->relationship('serverVariables', function (Builder $query) {
@@ -596,14 +634,16 @@ class EditServer extends EditRecord
->schema([
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"]))
->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
->label('Mounts')
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
->columnSpanFull(),
]),
Tab::make('Databases')
->hidden(fn () => !auth()->user()->can('viewList database'))
->icon('tabler-database')
->columns(4)
->schema([
Repeater::make('databases')
->grid()
@@ -617,44 +657,105 @@ class EditServer extends EditRecord
->formatStateUsing(fn ($record) => $record->database)
->hintAction(
Action::make('Delete')
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database))
->color('danger')
->icon('tabler-trash')
->action(fn (DatabaseManagementService $databaseManagementService, $record) => $databaseManagementService->delete($record))
->requiresConfirmation()
->modalIcon('tabler-database-x')
->modalHeading('Delete Database?')
->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
->modalDescription(fn (Get $get) => 'Are you sure you want to delete ' . $get('database') . '?')
->action(function (DatabaseManagementService $databaseManagementService, $record) {
$databaseManagementService->delete($record);
$this->fillForm();
})
),
TextInput::make('username')
->disabled()
->formatStateUsing(fn ($record) => $record->username)
->columnSpan(2),
->columnSpan(1),
TextInput::make('password')
->disabled()
->hintAction(
Action::make('rotate')
->icon('tabler-refresh')
->requiresConfirmation()
->action(fn (DatabasePasswordService $service, $record, $set, $get) => $this->rotatePassword($service, $record, $set, $get))
)
->formatStateUsing(fn (Database $database) => $database->password)
->columnSpan(2),
->password()
->revealable()
->columnSpan(1)
->hintAction(RotateDatabasePasswordAction::make())
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->disabled()
->formatStateUsing(fn ($record) => $record->remote)
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote)
->columnSpan(1)
->label('Connections From'),
TextInput::make('max_connections')
->disabled()
->formatStateUsing(fn ($record) => $record->max_connections)
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections)
->columnSpan(1),
TextInput::make('JDBC')
TextInput::make('jdbc')
->disabled()
->password()
->revealable()
->label('JDBC Connection String')
->columnSpan(2)
->formatStateUsing(fn (Get $get, $record) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($record->password) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database')),
->formatStateUsing(fn (Database $record) => $record->jdbc),
])
->relationship('databases')
->deletable(false)
->addable(false)
->columnSpan(4),
])->columns(4),
Forms\Components\Actions::make([
Action::make('createDatabase')
->authorize(fn () => auth()->user()->can('create database'))
->disabled(fn () => DatabaseHost::query()->count() < 1)
->label(fn () => DatabaseHost::query()->count() < 1 ? 'No Database Hosts' : 'Create Database')
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
->modalSubmitActionLabel('Create Database')
->action(function (array $data, DatabaseManagementService $service, Server $server, RandomWordService $randomWordService) {
if (empty($data['database'])) {
$data['database'] = $randomWordService->word() . random_int(1, 420);
}
if (empty($data['remote'])) {
$data['remote'] = '%';
}
$data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id);
try {
$service->setValidateDatabaseLimit(false)->create($server, $data);
} catch (Exception $e) {
Notification::make()
->title('Failed to Create Database')
->body($e->getMessage())
->danger()
->persistent()->send();
}
$this->fillForm();
})
->form([
Select::make('database_host_id')
->label('Database Host')
->required()
->placeholder('Select Database Host')
->options(fn (Server $server) => DatabaseHost::query()
->whereHas('nodes', fn ($query) => $query->where('nodes.id', $server->node_id))
->pluck('name', 'id')
)
->default(fn () => (DatabaseHost::query()->first())?->id)
->selectablePlaceholder(false),
TextInput::make('database')
->label('Database Name')
->alphaDash()
->prefix(fn (Server $server) => 's' . $server->id . '_')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Leaving this blank will auto generate a random name'),
TextInput::make('remote')
->columnSpan(1)
->regex('/^[\w\-\/.%:]+$/')
->label('Connections From')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Where connections should be allowed from. Leave blank to allow connections from anywhere.'),
]),
])->alignCenter()->columnSpanFull(),
]),
Tab::make('Actions')
->icon('tabler-settings')
->schema([
@@ -673,8 +774,8 @@ class EditServer extends EditRecord
Action::make('toggleInstall')
->label('Toggle Install Status')
->disabled(fn (Server $server) => $server->isSuspended())
->action(function (ServersController $serversController, Server $server) {
$serversController->toggleInstall($server);
->action(function (ToggleInstallService $service, Server $server) {
$service->handle($server);
$this->refreshFormData(['status', 'docker']);
}),
@@ -760,7 +861,7 @@ class EditServer extends EditRecord
->modalHeading('Are you sure you want to reinstall this server?')
->modalDescription('!! This can result in unrecoverable data loss !!')
->disabled(fn (Server $server) => $server->isSuspended())
->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)),
->action(fn (ReinstallServerService $service, Server $server) => $service->handle($server)),
])->fullWidth(),
ToggleButtons::make('')
->hint('This will reinstall the server with the assigned egg install script.'),
@@ -795,13 +896,13 @@ class EditServer extends EditRecord
->action(function (Server $server, ServerDeletionService $service) {
$service->handle($server);
return redirect(ListServers::getUrl());
return redirect(ListServers::getUrl(panel: 'admin'));
})
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
Actions\Action::make('console')
->label('Console')
->icon('tabler-terminal')
->url(fn (Server $server) => "/server/$server->uuid_short"),
->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)),
$this->getSaveFormAction()->formId('form'),
];
@@ -826,7 +927,7 @@ class EditServer extends EditRecord
public function getRelationManagers(): array
{
return [
ServerResource\RelationManagers\AllocationsRelationManager::class,
AllocationsRelationManager::class,
];
}
@@ -856,13 +957,4 @@ class EditServer extends EditRecord
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
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');
$set('password', $newPassword);
$set('JDBC', $jdbcString);
}
}

View File

@@ -1,8 +1,9 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use App\Filament\Server\Pages\Console;
use App\Filament\Admin\Resources\ServerResource;
use App\Models\Server;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
@@ -82,8 +83,8 @@ class ListServers extends ListRecords
->actions([
Action::make('View')
->icon('tabler-terminal')
->url(fn (Server $server) => "/server/$server->uuid_short")
->authorize(fn () => auth()->user()->can('view server')),
->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
->authorize(fn (Server $server) => auth()->user()->canAccessTenant($server)),
EditAction::make(),
])
->emptyStateIcon('tabler-brand-docker')

View File

@@ -1,13 +1,16 @@
<?php
namespace App\Filament\Resources\ServerResource\RelationManagers;
namespace App\Filament\Admin\Resources\ServerResource\RelationManagers;
use App\Filament\Admin\Resources\ServerResource\Pages\CreateServer;
use App\Models\Allocation;
use App\Models\Server;
use App\Services\Allocations\AssignmentService;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
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;
@@ -40,6 +43,7 @@ class AllocationsRelationManager extends RelationManager
public function table(Table $table): Table
{
return $table
->selectCurrentPageOnly()
->recordTitleAttribute('ip')
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
@@ -70,12 +74,13 @@ class AllocationsRelationManager extends RelationManager
CreateAction::make()->label('Create Allocation')
->createAnother(false)
->form(fn () => [
TextInput::make('allocation_ip')
->datalist($this->getOwnerRecord()->node->ipAddresses())
Select::make('allocation_ip')
->options(collect($this->getOwnerRecord()->node->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label('IP Address')
->inlineLabel()
->ipv4()
->helperText("Usually your machine's public IP unless you are port forwarding.")
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->required(),
TextInput::make('allocation_alias')
->label('Alias')
@@ -93,54 +98,9 @@ class AllocationsRelationManager extends RelationManager
->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());
}
})
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports',
CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip')))
)
->splitKeys(['Tab', ' ', ','])
->required(),
])
@@ -149,7 +109,8 @@ class AllocationsRelationManager extends RelationManager
->multiple()
->associateAnother(false)
->preloadRecordSelect()
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
->recordSelectSearchColumns(['ip', 'port'])
->label('Add Allocation'),
])
->bulkActions([

View File

@@ -1,9 +1,9 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers\ServersRelationManager;
use App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource\RelationManagers;
use App\Models\User;
use Filament\Resources\Resource;
@@ -23,7 +23,7 @@ class UserResource extends Resource
public static function getRelations(): array
{
return [
ServersRelationManager::class,
RelationManagers\ServersRelationManager::class,
];
}
@@ -31,6 +31,7 @@ class UserResource extends Resource
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Models\Role;
use App\Services\Users\UserCreationService;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
protected static bool $canCreateAnother = false;
private UserCreationService $service;
public function boot(UserCreationService $service): void
{
$this->service = $service;
}
public function form(Form $form): Form
{
return $form
->columns(['default' => 1, 'lg' => 3])
->schema([
TextInput::make('username')
->alphaNum()
->required()
->unique()
->minLength(3)
->maxLength(255),
TextInput::make('email')
->email()
->required()
->unique()
->maxLength(255),
TextInput::make('password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
->password(),
CheckboxList::make('roles')
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
->relationship('roles', 'name')
->dehydrated()
->label('Admin Roles')
->columnSpanFull()
->bulkToggleable(false),
]);
}
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
protected function handleRecordCreation(array $data): Model
{
$data['root_admin'] = false;
$roles = $data['roles'];
$roles = collect($roles)->map(fn ($role) => Role::findById($role));
unset($data['roles']);
$user = $this->service->handle($data);
$user->syncRoles($roles);
return $user;
}
}

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use App\Filament\Admin\Resources\UserResource;
use App\Models\Role;
use App\Models\User;
use Filament\Actions\DeleteAction;
@@ -24,19 +24,25 @@ class EditUser extends EditRecord
return $form
->schema([
Section::make()->schema([
TextInput::make('username')->required()->maxLength(255),
TextInput::make('email')->email()->required()->maxLength(255),
TextInput::make('username')
->required()
->minLength(3)
->maxLength(255),
TextInput::make('email')
->email()
->required()
->maxLength(255),
TextInput::make('password')
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
->dehydrated(fn (?string $state): bool => filled($state))
->required(fn (string $operation): bool => $operation === 'create')
->password(),
Select::make('language')
->required()
->hidden()
->default('en')
->options(fn (User $user) => $user->getAvailableLanguages()),
Hidden::make('skipValidation')->default(true),
Hidden::make('skipValidation')
->default(true),
CheckboxList::make('roles')
->disabled(fn (User $user) => $user->id === auth()->user()->id)
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
@@ -44,7 +50,8 @@ class EditUser extends EditRecord
->label('Admin Roles')
->columnSpanFull()
->bulkToggleable(false),
])->columns(),
])
->columns(['default' => 1, 'lg' => 3]),
]);
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Models\User;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
public function table(Table $table): Table
{
return $table
->searchable(false)
->columns([
ImageColumn::make('picture')
->visibleFrom('lg')
->label('')
->extraImgAttributes(['class' => 'rounded-full'])
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
TextColumn::make('external_id')
->searchable()
->hidden(),
TextColumn::make('uuid')
->label('UUID')
->hidden()
->searchable(),
TextColumn::make('username')
->searchable(),
TextColumn::make('email')
->searchable()
->icon('tabler-mail'),
IconColumn::make('use_totp')
->label('2FA')
->visibleFrom('lg')
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
->boolean()
->sortable(),
TextColumn::make('roles.name')
->label('Roles')
->badge()
->icon('tabler-users-group')
->placeholder('No roles'),
TextColumn::make('servers_count')
->counts('servers')
->icon('tabler-server')
->label('Servers'),
TextColumn::make('subusers_count')
->visibleFrom('sm')
->label('Subusers')
->counts('subusers')
->icon('tabler-users'),
])
->actions([
EditAction::make(),
])
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete user')),
]),
]);
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label('Create User'),
];
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Resources\UserResource\RelationManagers;
namespace App\Filament\Admin\Resources\UserResource\RelationManagers;
use App\Enums\ServerState;
use App\Models\Server;

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Models\WebhookConfiguration;
use Filament\Resources\Resource;
class WebhookResource extends Resource
{
protected static ?string $model = WebhookConfiguration::class;
protected static ?string $modelLabel = 'Webhook';
protected static ?string $pluralModelLabel = 'Webhooks';
protected static ?string $navigationIcon = 'tabler-webhook';
protected static ?string $navigationGroup = 'Advanced';
protected static ?string $recordTitleAttribute = 'description';
public static function getNavigationBadge(): ?string
{
return static::getModel()::count() ?: null;
}
public static function getPages(): array
{
return [
'index' => Pages\ListWebhookConfigurations::route('/'),
'create' => Pages\CreateWebhookConfiguration::route('/create'),
'edit' => Pages\EditWebhookConfiguration::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource;
use App\Models\WebhookConfiguration;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
class CreateWebhookConfiguration extends CreateRecord
{
protected static string $resource = WebhookResource::class;
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
];
}
protected function getFormActions(): array
{
return [];
}
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('endpoint')
->activeUrl()
->required(),
TextInput::make('description')
->required(),
CheckboxList::make('events')
->lazy()
->options(fn () => WebhookConfiguration::filamentCheckboxList())
->searchable()
->bulkToggleable()
->columns(3)
->columnSpanFull()
->gridDirection('row')
->required(),
]);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Models\WebhookConfiguration;
use App\Filament\Admin\Resources\WebhookResource;
use Filament\Actions;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\EditRecord;
class EditWebhookConfiguration extends EditRecord
{
protected static string $resource = WebhookResource::class;
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('endpoint')
->label('Endpoint')
->activeUrl()
->required(),
TextInput::make('description')
->label('Description')
->required(),
CheckboxList::make('events')
->label('Events')
->lazy()
->options(fn () => WebhookConfiguration::filamentCheckboxList())
->searchable()
->bulkToggleable()
->columns(3)
->columnSpanFull()
->gridDirection('row')
->required(),
]);
}
protected function getFormActions(): array
{
return [];
}
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make()
->label('Delete')
->modalHeading('Are you sure you want to delete this?')
->modalDescription('')
->modalSubmitActionLabel('Delete'),
$this->getSaveFormAction()->formId('form'),
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource;
use App\Models\WebhookConfiguration;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\DeleteAction;
class ListWebhookConfigurations extends ListRecords
{
protected static string $resource = WebhookResource::class;
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('description')
->label('Description'),
TextColumn::make('endpoint')
->label('Endpoint'),
])
->actions([
DeleteAction::make()
->label('Delete'),
EditAction::make()
->label('Edit'),
])
->emptyStateIcon('tabler-webhook')
->emptyStateDescription('')
->emptyStateHeading('No Webhooks')
->emptyStateActions([
CreateAction::make('create')
->label('Create Webhook')
->button(),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->label('Create Webhook')
->hidden(fn () => WebhookConfiguration::count() <= 0),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Filament\App\Resources;
use App\Filament\App\Resources\ServerResource\Pages;
use App\Models\Server;
use Filament\Resources\Resource;
class ServerResource extends Resource
{
protected static ?string $model = Server::class;
protected static ?string $slug = '/';
protected static bool $shouldRegisterNavigation = false;
public static function canAccess(): bool
{
return true;
}
public static function getPages(): array
{
return [
'index' => Pages\ListServers::route('/'),
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Filament\App\Resources\ServerResource\Pages;
use App\Filament\App\Resources\ServerResource;
use App\Filament\Components\Tables\Columns\ServerEntryColumn;
use App\Filament\Server\Pages\Console;
use App\Models\Server;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class ListServers extends ListRecords
{
protected static string $resource = ServerResource::class;
public function table(Table $table): Table
{
$baseQuery = auth()->user()->can('viewList server') ? Server::query() : auth()->user()->accessibleServers();
return $table
->paginated(false)
->query(fn () => $baseQuery)
->poll('15s')
->columns([
Stack::make([
ServerEntryColumn::make('server_entry')
->searchable(['name']),
]),
])
->contentGrid([
'default' => 1,
'md' => 2,
])
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
->emptyStateIcon('tabler-brand-docker')
->emptyStateDescription('')
->emptyStateHeading('You don\'t have access to any servers!')
->persistFiltersInSession()
->filters([
TernaryFilter::make('only_my_servers')
->label('Owned by')
->placeholder('All servers')
->trueLabel('My Servers')
->falseLabel('Others\' Servers')
->default()
->queries(
true: fn (Builder $query) => $query->where('owner_id', auth()->user()->id),
false: fn (Builder $query) => $query->whereNot('owner_id', auth()->user()->id),
blank: fn (Builder $query) => $query,
),
SelectFilter::make('egg')
->relationship('egg', 'name', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('egg_id')))
->searchable()
->preload(),
]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Filament\Components\Actions;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use Filament\Actions\Action;
class ExportEggAction extends Action
{
public static function getDefaultName(): ?string
{
return 'export';
}
protected function setUp(): void
{
parent::setUp();
$this->label('Export');
$this->authorize(fn () => auth()->user()->can('export egg'));
$this->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json'));
}
}

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