Compare commits

...

156 Commits

Author SHA1 Message Date
Boy132
0a04c76d66 delete all (remote) backups when server is deleted 2026-04-28 14:05:28 +02:00
Boy132
1a2e155302 Merge remote-tracking branch 'origin/main' into boy132/backup-hosts 2026-04-28 11:57:55 +02:00
Boy132
4fdbbff74b Correctly store falsy startup variable values (#2305) 2026-04-28 09:35:19 +02:00
Boy132
2ed891633c Add subuser permission for changing server icon (#2304) 2026-04-28 09:34:38 +02:00
Michael (Parker) Parker
58dcbeac0b install plugin packages on start (#2298) 2026-04-23 08:41:44 -05:00
Boy132
91c5ddb2bd Catch icon exception when importing egg (#2299) 2026-04-22 13:52:31 +02:00
Lance Pioch
562be98b20 Add user_mountable functionality for Mounts (#2077) (#2181)
Co-authored-by: Boy132 <mail@boy132.de>
2026-04-21 22:03:10 -04:00
Lance Pioch
fd3b8a7ab3 Laravel 12.56.0 Shift (#2283)
Co-authored-by: Shift <shift@laravelshift.com>
2026-04-21 22:02:48 -04:00
Boy132
1817383bf5 Add changes from upstream (#2293)
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
Co-authored-by: danny6167 <danielb@purpleflaghosting.com>
Co-authored-by: MrSoulPenguin <28676680+MrSoulPenguin@users.noreply.github.com>
2026-04-20 17:25:54 +02:00
Boy132
d39a0c4464 Allow to add translation prefix to custom subuser permissions (#2265) 2026-04-20 17:24:24 +02:00
Boy132
2e48095379 Refactor egg & server icon uploading (#2281) 2026-04-20 17:23:41 +02:00
Jørgen Vatle
06c662988a Feature: Add support for user-configurable egg/nests index file. (#2287)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2026-04-20 05:06:39 -04:00
Neil Hanlon
98d7158dfc Fix PATCH /api/application/nodes returning 500 due to null timestamps (#2286) 2026-04-20 05:06:01 -04:00
JUBE
e01d9f2cf3 Add sortable feature to name column in ListServers (#2282) 2026-04-08 19:50:01 -04:00
Boy132
b693d0e728 Do not allow all file extensions for egg image in egg importer (#2279) 2026-03-22 19:18:46 +01:00
Boy132
7a8aaad66b Merge remote-tracking branch 'origin/main' into boy132/backup-hosts 2026-03-17 15:33:38 +01:00
Boy132
b7aea4c26e avoid n+1 2026-03-17 15:30:53 +01:00
Boy132
612041e1f8 Run composer update and update model phpdocs (#2275) 2026-03-17 09:09:01 +01:00
Boy132
64bcdb514b Add jobs for plugin install, update and uninstall (#2267) 2026-03-17 09:08:28 +01:00
Lance Pioch
c1105db702 Fix encodings with new tested helper (#2238) 2026-03-16 16:48:13 -04:00
Lance Pioch
761492d09f Laravel 12.53.0 Shift (#2266)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-26 23:56:15 -05:00
Boy132
cdd69c29c5 rabbit fixes 2 2026-02-24 15:24:20 +01:00
Boy132
5057d72cb7 rabbit fixes 2026-02-24 15:06:25 +01:00
Boy132
f30025994e Merge remote-tracking branch 'origin/main' into boy132/backup-hosts 2026-02-24 14:17:57 +01:00
Boy132
1a9cc5f565 re-implement backup transfers 2026-02-24 14:15:26 +01:00
Boy132
a9f6bcb1e8 add simple BackupsRelationManager 2026-02-24 14:04:18 +01:00
Boy132
a6ba81eb2d small cleanup 2026-02-24 13:44:04 +01:00
Boy132
c3b597db9b fix function name 2026-02-24 13:40:29 +01:00
Boy132
a949a565a8 add info comment 2026-02-24 13:36:59 +01:00
Boy132
bc727b72fd use TablerIcon enum and update some actions 2026-02-24 13:32:56 +01:00
Boy132
e35ce1e79d Use folder name as id when having id mismatch to allow deletion (#2263) 2026-02-23 20:25:19 +01:00
Charles
42c127c004 Fix invisible force delete button (#2262) 2026-02-23 13:41:15 -05:00
Boy132
593f209142 Fix stock egg UUID migration (#2259) 2026-02-22 17:23:51 +01:00
Boy132
9bf5b2cf0a Do not throw error when checking for egg updates (#2256) 2026-02-22 17:23:09 +01:00
Lance Pioch
f76e864a30 Laravel 12.52.0 Shift (#2247)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-22 11:11:18 -05:00
Boy132
01cfa31ee1 Limit activity logs on profile page (#2253) 2026-02-19 14:57:48 +01:00
Boy132
dead664e4d Make plugin id in artisan commands also case insensitive (#2252) 2026-02-19 14:57:19 +01:00
Boy132
c215e95133 Merge branch 'main' into boy132/backup-hosts 2026-02-19 13:56:23 +01:00
Charles
1bbbcd0e25 Update server/egg icon url supported file types (#2249) 2026-02-18 16:30:24 -05:00
Michael (Parker) Parker
677d2f742c docker env fixes (#2234)
Co-authored-by: Charles <charles@pelican.dev>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2026-02-18 14:40:46 -05:00
Charles
650fb16d2d exclude bulk actions in list eggs (#2239) 2026-02-17 06:17:28 -05:00
Boy132
58814ea782 Allow to delete errored plugins (#2246) 2026-02-17 11:52:25 +01:00
hallo123wert
0918ed308b exclude bulk actions (#2240) 2026-02-16 06:34:14 -05:00
Lance Pioch
85d5f2ec3f Fix alert banners (#1492) (#2177) 2026-02-15 15:34:09 -05:00
Lance Pioch
810f237547 Remove ignition, just use new default error handler (#2241) 2026-02-15 15:03:55 -05:00
Lance Pioch
8f191890a1 Fix null backup limit exception (#2242) 2026-02-15 15:03:37 -05:00
Boy132
160e0e54f5 More action fixes (#2237)
Co-authored-by: notCharles <charles@pelican.dev>
2026-02-15 17:31:50 +01:00
Charles
fdcfbb00ca Revert "Fix UTF double encoding in file editor" (#2236) 2026-02-15 11:08:01 -05:00
Lance Pioch
44f6cf8928 Fix UTF double encoding in file editor (#2199) 2026-02-14 10:33:35 -05:00
Boy132
cf2a26bbf0 Improve plugin loading (#2233) 2026-02-14 14:06:50 +01:00
gOOvER
adb6678eee Convert YAML boolean defaults to strings (#2232) 2026-02-13 18:12:44 -05:00
Lance Pioch
9539e21b39 Handle invalid svg icon in generic-oidc provider gracefully (#2216) 2026-02-13 10:06:07 -05:00
Charles
33660f635f Fix yet another action (#2230) 2026-02-12 21:00:26 -05:00
Lance Pioch
8c475ed95f Add “reachable” column for Client -> Wings connections for Nodes (#2200) 2026-02-12 17:06:38 -05:00
Lance Pioch
d43cb1d180 Fix empty egg config_files causing fatal 500 error (#2195) (#2197) 2026-02-12 17:06:19 -05:00
Lance Pioch
fe55dbd200 Ignore oauth redirects for spa (#2224) 2026-02-12 16:31:35 -05:00
Charles
0bb4503c2b fix unsuspend button (#2227) 2026-02-12 16:31:20 -05:00
Lance Pioch
3c1168beb5 Update job runners (#2225) 2026-02-12 10:29:33 -05:00
Quinten
1a092aedc8 Docker: chown composer.json and composer.lock for plugins (#2220) 2026-02-12 10:02:12 -05:00
Charles
8c99a8030f Fix more actions (#2208) 2026-02-12 09:56:56 -05:00
Lance Pioch
6e53b1cd7d Allow custom ips to still be entered (#2223) 2026-02-12 09:49:54 -05:00
Boy132
4f2a4726a2 Merge branch 'main' into boy132/backup-hosts 2026-02-12 08:40:54 +01:00
Charles
4042e0416b Revert toolbar change (#2218) 2026-02-11 17:54:58 -05:00
Lance Pioch
cc8973cf00 Laravel 12.51.0 Shift (#2213)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-11 11:18:33 -05:00
Quinten
8ebe75b947 fix: composer not been installed in the docker image (#2211) 2026-02-10 16:00:18 -05:00
Hythera
f8144407d1 fix: composer content hash (#2209) 2026-02-09 22:26:01 -05:00
Charles
e431ccb66a add rounding to list-files header (#2207) 2026-02-09 22:19:10 -05:00
Lance Pioch
9291bb4477 Fix webhook processing for event objects (#2198) 2026-02-09 08:47:09 -05:00
Lance Pioch
e8c80ae420 Fix SFTP access denied for subuser when view role is assigned (#2196) 2026-02-09 08:46:47 -05:00
stdpi
f1be003276 Change file browser sticky header again... (#2203) 2026-02-08 15:17:48 -05:00
Charles
e532a9a180 composer upgrade (#2167)
Co-authored-by: Lance Pioch <git@lance.sh>
2026-02-07 14:33:51 -05:00
Charles
41fdd7bc8e Exclude create client api key button (#2169) 2026-02-07 14:33:32 -05:00
Lance Pioch
9b01203b7c Add per-user toggle to redirect admins to /admin after login (#2191) 2026-02-07 13:21:14 -05:00
Lance Pioch
ab4eadec32 Fix Exporting an egg in yaml format (#2172) 2026-02-07 08:10:00 -05:00
Lance Pioch
789c4c7284 Localize email notifications (#2043) (#2178) 2026-02-06 10:37:15 -05:00
Lance Pioch
b1a39f1724 Handle X-Forwarded-Proto in .htaccess for SSL-terminating proxies (#2171) 2026-02-06 09:54:24 -05:00
Lance Pioch
6a548c09a0 Clarify OAuth error when provider account has no linked email (#2179) 2026-02-06 07:50:37 -05:00
Lance Pioch
55bda569cc Implement flexible caching for node statuses (#2174) 2026-02-06 07:50:20 -05:00
Lance Pioch
adf1249086 Fix Egg Feature modals not working (#2175) 2026-02-06 07:49:56 -05:00
Lance Pioch
dbf77bf146 Implement single file move to support Unix mv semantics (#1984) (#2176) 2026-02-06 07:49:40 -05:00
Frogperson
a34bf9fd49 Add Daemon Base Directory field (#2151)
Co-authored-by: Boy132 <mail@boy132.de>
2026-02-05 08:00:25 -05:00
Boy132
7a9deba0e1 Fix notifications for DeleteAction on EditEgg page (#2165) 2026-02-04 22:20:22 +01:00
Boy132
e3893ff872 Merge branch 'main' into boy132/backup-hosts 2026-02-04 21:47:59 +01:00
Charles
159bfe2210 exclude node actions (#2164) 2026-02-04 06:48:13 -05:00
stdpi
a821db8aae Improve file browser UI (#2086) 2026-02-04 05:40:45 -05:00
Lance Pioch
1556f8efb8 Allow all permissions to be toggled at once for api tokens (#2154) 2026-02-02 08:41:10 -05:00
Lance Pioch
57c2aa6f21 Fix the tabbing for username to password on login (#2155)
Co-authored-by: notCharles <charles@pelican.dev>
2026-02-02 08:40:55 -05:00
Charles
36de4c3786 composer update (#2161) 2026-01-31 19:37:54 -05:00
Boy132
26312e3897 Add basic api for plugins (#2146) 2026-02-01 00:10:57 +01:00
Boy132
a477c89025 Move username sanitization to model and make it less strict (#2150) 2026-02-01 00:07:26 +01:00
Boy132
93e81c26a9 Send server id to wings (#2157) 2026-01-31 22:54:06 +01:00
Boy132
23e91e8df3 Fix "visit website" button being invisible (#2158) 2026-01-31 22:53:56 +01:00
Boy132
833294bfaf Invisible button and tooltip fixes (#2149) 2026-01-29 15:37:05 +01:00
Boy132
3a9f09c188 run pint 2026-01-28 09:07:40 +01:00
Boy132
9252b21205 Merge branch 'main' into boy132/backup-hosts
# Conflicts:
#	app/Filament/Admin/Pages/Settings.php
#	app/Filament/Admin/Resources/Servers/Pages/EditServer.php
2026-01-28 09:04:39 +01:00
Lance Pioch
abaeeff86d Laravel 12.49.0 Shift (#2145)
Co-authored-by: Shift <shift@laravelshift.com>
2026-01-27 23:40:20 -05:00
Charles
dd77555c42 Add tooltips to actions across admin area (#2134)
Co-authored-by: Boy132 <mail@boy132.de>
2026-01-27 20:07:18 -05:00
Boy132
297ecb544d Replace icon strings with enum (#2113) 2026-01-27 11:36:07 +01:00
Boy132
e14bb7d030 Fix oauth provider "enabled" checks (#2142) 2026-01-27 11:27:19 +01:00
Boy132
6c8c2a0b91 Merge branch 'main' into boy132/backup-hosts
# Conflicts:
#	app/Enums/RolePermissionModels.php
2026-01-23 16:51:24 +01:00
Boy132
c770937880 Migration to convert former stock egg uuids (#2108) 2026-01-23 16:40:24 +01:00
Boy132
426643eaa6 Add allocation to role permission models & make sure user can target node of allocation (#2124) 2026-01-23 16:37:01 +01:00
Boy132
3ca0f64e6e Set failed plugin installs to "not_installed" instead of errored (#2129) 2026-01-23 16:36:38 +01:00
Boy132
7da9d8c21d rabbit fixes 2026-01-20 11:22:38 +01:00
Boy132
f1dbbbb7b0 fix migration for mysql 2026-01-20 10:40:23 +01:00
Boy132
150f8035d6 fix tests 2026-01-20 10:36:32 +01:00
Boy132
efebb999df fix backup hosts for "all nodes" 2026-01-20 10:07:48 +01:00
Boy132
53761f8b21 dont allow to delete last backup host 2026-01-20 09:52:26 +01:00
Boy132
a181978a96 handle old backups and cleanup 2026-01-20 09:45:35 +01:00
Boy132
12d8b23c98 Merge remote-tracking branch 'origin/main' into boy132/backup-hosts 2026-01-20 08:46:18 +01:00
Michael (Parker) Parker
8e8ce3b50f fix plugins in entrypoint (#2122) 2026-01-19 09:15:18 -05:00
Charles
b1e9cadc10 Revert "Update to filament v5, Livewire v4" (#2121) 2026-01-18 17:17:23 -05:00
Charles
7bf1f18c2d Update to filament v5, Livewire v4 (#2114)
Co-authored-by: Lance Pioch <git@lance.sh>
2026-01-18 17:04:13 -05:00
Charles
6fe7d29960 composer update (#2120) 2026-01-18 16:44:16 -05:00
Charles
15172b1d86 Add github eggs to egg importer (#2116) 2026-01-18 16:33:09 -05:00
Boy132
9f744d39a2 Add traits for customizing tabs (#2101) 2026-01-18 22:32:18 +01:00
Boy132
b79511568e Fix allocation policy for admins and update checks (#2090) 2026-01-18 22:26:15 +01:00
Lance Pioch
adeb1b4217 Add parallel flags to github ci (#2109) 2026-01-18 16:24:39 -05:00
JoanFo
d064bf9734 Allow backup transfers (#2068)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2026-01-18 16:23:21 -05:00
Michael (Parker) Parker
107286d618 Multiple Container Fixes (#2063)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2026-01-18 16:21:33 -05:00
Lance Pioch
a3203f7dda Update readme.md 2026-01-16 19:14:05 -05:00
Boy132
ad2333ea9d more work on backup hosts 2026-01-16 23:04:18 +01:00
Boy132
dd4e7231d0 start backup hosts 2026-01-16 21:52:24 +01:00
Boy132
e9abd56f7a Add traits for customizing wizard steps (#2100) 2026-01-15 20:55:53 +01:00
PalmarHealer
675ab057b0 fix: Enhance feedback notifications for egg actions (#2042)
Co-authored-by: Charles <charles@pelican.dev>
2026-01-15 12:32:50 -05:00
Boy132
943d9d3ef5 Update translations from crowdin (#2110) 2026-01-15 07:59:55 -05:00
Lance Pioch
c06a525be2 Laravel 12.47.0 Shift (#2103)
Co-authored-by: Shift <shift@laravelshift.com>
2026-01-15 07:57:57 -05:00
Boy132
2ff5fdf831 Fix columns for mount form (#2105) 2026-01-15 13:57:37 +01:00
Boy132
0e810f3110 Throw yarn errors when installing themes (#2104) 2026-01-14 08:23:24 +01:00
Charles
eadbe6e8fd fix client side view database unlimited state (#2047)
Co-authored-by: Boy132 <mail@boy132.de>
2026-01-13 05:33:20 -05:00
Boy132
53aa49b11a Add changes from upstream (#2076)
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
2026-01-13 08:39:50 +01:00
Boy132
6ae4f007c8 Make sure custom pages/relations don't override default pages/relations (#2099) 2026-01-12 18:00:37 +01:00
Boy132
6b9d683f06 Update database config to remove deprecation warning on php 8.5 (#2089) 2026-01-09 14:39:22 +01:00
Boy132
3b24e22316 Set plugin status to "errored" if it errored (#2084) 2026-01-08 17:43:31 +01:00
Boy132
bd012f52a9 Add tests for php 8.5 (#2079) 2026-01-08 17:32:23 +01:00
Boy132
af202d9827 Add user to shouldLink and shouldCreate oauth functions (#2083) 2026-01-08 15:13:15 +01:00
Boy132
6ebeb40ba0 Make rule for user language less restrictive (#2075) 2026-01-06 08:45:53 +01:00
Boy132
333eeda065 Disable field if server variable is not user_editable (#2074) 2026-01-06 08:45:40 +01:00
MartinOscar
fcfafadec7 Return if no egg was selected in the Installer (#2073) 2026-01-05 14:21:34 +01:00
Boy132
76b6118fd1 Fix typo in method name (#2062) 2026-01-04 15:17:48 -05:00
PalmarHealer
3141fe61b4 fix: plugin migration rollback and cache clearing on uninstall (#2033)
Co-authored-by: Boy132 <mail@boy132.de>
2026-01-03 23:44:33 +01:00
Charles
bed9dbeb2b Add Eggs to Installer (#2004)
Co-authored-by: Boy132 <mail@boy132.de>
2025-12-29 17:24:02 -05:00
Boy132
976cb00c0d Replace Artisan::call in plugin service for better error handling (#2031) 2025-12-28 14:44:39 +01:00
Quinten
e3534bbb29 Bungeecord: Fix Download (#2055) 2025-12-28 13:48:22 +01:00
xDev789
5740c93032 Per request cache for permission checks (#2029)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2025-12-28 02:00:59 +01:00
MartinOscar
d72e075977 chore: Prevent users from caching Config (#2048) 2025-12-28 01:50:36 +01:00
Boy132
9af608f808 Fix relation managers for admin server resource (#2050) 2025-12-25 00:44:30 +01:00
Boy132
ac36e7a4b5 Fix oauth providers with no color (#2044) 2025-12-24 14:38:47 +01:00
Boy132
b1c64e2ef1 Add error notification when plugin install, update or uninstall fails (#2032) 2025-12-24 14:38:25 +01:00
PalmarHealer
da2e930d4d Correct bounty link (#2039)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-12-23 15:46:00 -05:00
Charles
460a5dfaf8 composer update (#2030) 2025-12-22 19:01:52 -05:00
killerbite95
576f04be58 fix: use correct log path for upload action (#2016)
Co-authored-by: Charles <charles@pelican.dev>
2025-12-22 19:01:44 -05:00
Boy132
43fb030133 Don't log yarn exceptions as error but warning (#2022) 2025-12-21 15:37:21 +01:00
Boy132
ae054f6e9b Fix actions when plugin is "errored" (#2027) 2025-12-21 15:37:07 +01:00
Boy132
fef91791c3 Fix plugin settings not showing on non-admin plugins (#2023) 2025-12-21 15:36:39 +01:00
Boy132
1d5ace3a6d Clear filament cache when installing a plugin (#2017) 2025-12-20 02:00:57 +01:00
1135 changed files with 31627 additions and 15625 deletions

View File

@@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
php: [8.3, 8.4, 8.5]
env:
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
@@ -61,14 +61,17 @@ jobs:
- name: Create SQLite file
run: touch database/testing.sqlite
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests
run: vendor/bin/pest tests/Unit
run: vendor/bin/pest tests/Unit --parallel
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
run: vendor/bin/pest tests/Integration --parallel
mysql:
name: MySQL
@@ -76,8 +79,8 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
database: ["mysql:8"]
php: [8.5]
database: ["mysql:8.4", "mysql:9.6"]
services:
database:
image: ${{ matrix.database }}
@@ -120,14 +123,20 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
- name: Unit tests
run: vendor/bin/pest tests/Unit
run: vendor/bin/pest tests/Unit --parallel
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
run: vendor/bin/pest tests/Integration --parallel
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@@ -138,8 +147,8 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
php: [8.5]
database: ["mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}
@@ -182,14 +191,20 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
- name: Unit tests
run: vendor/bin/pest tests/Unit
run: vendor/bin/pest tests/Unit --parallel
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
run: vendor/bin/pest tests/Integration --parallel
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@@ -200,8 +215,8 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
database: ["postgres:14"]
php: [8.5]
database: ["postgres:17", "postgres:18"]
services:
database:
image: ${{ matrix.database }}
@@ -238,6 +253,7 @@ jobs:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -250,11 +266,14 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests
run: vendor/bin/pest tests/Unit
run: vendor/bin/pest tests/Unit --parallel
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
run: vendor/bin/pest tests/Integration --parallel

View File

@@ -3,7 +3,7 @@ name: Lint
on:
pull_request:
branches:
- '**'
- "**"
jobs:
pint:
@@ -16,7 +16,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
php-version: "8.4"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [ 8.2, 8.3, 8.4 ]
php: [8.2, 8.3, 8.4, 8.5]
steps:
- name: Code Checkout
uses: actions/checkout@v4

View File

@@ -38,7 +38,7 @@ RUN yarn config set network-timeout 300000 \
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --exclude=docker/ . ./
RUN composer dump-autoload --optimize
@@ -50,7 +50,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
@@ -62,38 +62,35 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic fcgi
# packages for running the panel
caddy ca-certificates supervisor supercronic fcgi \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /pelican-data/plugins /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
&& ln -s /pelican-data/plugins /var/www/html/plugins \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Copy composer binary for runtime plugin dependency management
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
# Create and remove directories
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
&& rm -rf /var/www/html/plugins \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env /var/www/html/.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -s /pelican-data/storage /var/www/html/public/storage \
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
&& ln -s /pelican-data/plugins /var/www/html \
# Allow www-data write permissions where necessary
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/crontab /etc/crontabs/crontab
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh

View File

@@ -5,6 +5,6 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN install-php-extensions bcmath gd intl zip pcntl pdo_mysql pdo_pgsql bz2
RUN rm /usr/local/bin/install-php-extensions

View File

@@ -5,7 +5,7 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN install-php-extensions bcmath gd intl zip pcntl pdo_mysql pdo_pgsql bz2
RUN rm /usr/local/bin/install-php-extensions
@@ -42,7 +42,7 @@ RUN yarn config set network-timeout 300000 \
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --exclude=docker/ . ./
RUN composer dump-autoload --optimize
@@ -54,7 +54,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
@@ -68,36 +68,35 @@ WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic fcgi coreutils
# packages for running the panel
caddy ca-certificates supervisor supercronic fcgi coreutils \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Copy composer binary for runtime plugin dependency management
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
# Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /pelican-data/plugins /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
&& ln -s /pelican-data/plugins /var/www/html/plugins \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Create and remove directories
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
&& rm -rf /var/www/html/plugins \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env /var/www/html/.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -s /pelican-data/storage /var/www/html/public/storage \
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
&& ln -s /pelican-data/plugins /var/www/html \
# Allow www-data write permissions where necessary
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/crontab /etc/crontabs/crontab
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands\Dev;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class GenerateTablerIconsEnum extends Command
{
protected $signature = 'dev:generate-tabler-icons-enum';
protected $description = 'Generate an enum for tabler icons based on the secondnetwork/blade-tabler-icons svgs';
public function handle(): void
{
$files = File::files(base_path('vendor/secondnetwork/blade-tabler-icons/resources/svg'));
$files = array_filter($files, fn ($file) => $file->getExtension() === 'svg');
$enumContent = "<?php\n\n";
$enumContent .= "namespace App\\Enums;\n\n";
$enumContent .= "enum TablerIcon: string\n{\n";
foreach ($files as $file) {
$filename = pathinfo($file->getFilename(), PATHINFO_FILENAME);
// Letter V is duplicate, as "letter-v" and "letter-letter-v"
if (str($filename)->contains('letter-letter')) {
continue;
}
// Filled icons exist with "-f" and "-filled", we only want the later
if (str($filename)->endsWith('-f') && file_exists(base_path("vendor/secondnetwork/blade-tabler-icons/resources/svg/{$filename}illed.svg"))) {
continue;
}
$caseName = str($filename)->title()->replace('-', '');
$value = str($filename)->slug()->prepend('tabler-');
$enumContent .= " case $caseName = '$value';\n";
}
$enumContent .= "}\n";
File::put(base_path('app/Enums/TablerIcon.php'), $enumContent);
$this->info('Enum generated');
}
}

View File

@@ -8,7 +8,6 @@ use App\Services\Eggs\Sharing\EggExporterService;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use JsonException;
use Symfony\Component\Yaml\Yaml;
class CheckEggUpdatesCommand extends Command
@@ -22,14 +21,12 @@ class CheckEggUpdatesCommand extends Command
try {
$this->check($egg, $exporterService);
} catch (Exception $exception) {
$this->error("{$egg->name}: Error ({$exception->getMessage()})");
$this->error("$egg->name: Error ({$exception->getMessage()})");
}
}
}
/**
* @throws JsonException
*/
/** @throws Exception */
private function check(Egg $egg, EggExporterService $exporterService): void
{
if (is_null($egg->update_url)) {
@@ -45,7 +42,13 @@ class CheckEggUpdatesCommand extends Command
? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML))
: json_decode($exporterService->handle($egg->id, EggFormat::JSON), true);
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url)->throw()->body();
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url);
if ($remote->failed()) {
throw new Exception("HTTP request returned status code {$remote->status()}");
}
$remote = $remote->body();
$remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
unset($local['exported_at'], $remote['exported_at']);

View File

@@ -13,7 +13,7 @@ class UpdateEggIndexCommand extends Command
public function handle(): int
{
try {
$data = Http::timeout(5)->connectTimeout(1)->get('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json')->throw()->json();
$data = Http::timeout(5)->connectTimeout(1)->get(config('panel.cdn.egg_index_url'))->throw()->json();
} catch (Exception $exception) {
$this->error($exception->getMessage());

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Console\Commands\Overrides;
use Illuminate\Foundation\Console\ConfigCacheCommand as BaseConfigCacheCommand;
class ConfigCacheCommand extends BaseConfigCacheCommand
{
/**
* Prevent config from being cached
*/
public function handle()
{
$this->components->warn('Configuration caching has been disabled.');
$this->line(' Reason: This application uses dynamic plugins. Caching config');
$this->line(' prevents /plugins/config/*.php files from being loaded correctly.');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Console\Commands\Overrides;
use Illuminate\Foundation\Console\OptimizeCommand as BaseOptimizeCommand;
class OptimizeCommand extends BaseOptimizeCommand
{
/**
* Prevent config from being cached
*
* @return array<string, string>
*/
protected function getOptimizeTasks()
{
return array_except(parent::getOptimizeTasks(), 'config');
}
}

View File

@@ -16,7 +16,7 @@ class DisablePluginCommand extends Command
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands\Plugin;
use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class InstallPluginCommand extends Command
@@ -17,7 +18,7 @@ class InstallPluginCommand extends Command
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');
@@ -31,8 +32,12 @@ class InstallPluginCommand extends Command
return;
}
$pluginService->installPlugin($plugin);
try {
$pluginService->installPlugin($plugin);
$this->info('Plugin installed and enabled.');
$this->info('Plugin installed and enabled.');
} catch (Exception $exception) {
$this->error('Could not install plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands\Plugin;
use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class UninstallPluginCommand extends Command
@@ -17,7 +18,7 @@ class UninstallPluginCommand extends Command
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');
@@ -36,8 +37,12 @@ class UninstallPluginCommand extends Command
$deleteFiles = $this->confirm('Do you also want to delete the plugin files?');
}
$pluginService->uninstallPlugin($plugin, $deleteFiles);
try {
$pluginService->uninstallPlugin($plugin, $deleteFiles);
$this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.');
$this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.');
} catch (Exception $exception) {
$this->error('Could not uninstall plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands\Plugin;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class UpdatePluginCommand extends Command
@@ -16,7 +17,7 @@ class UpdatePluginCommand extends Command
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find($id);
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');
@@ -30,8 +31,12 @@ class UpdatePluginCommand extends Command
return;
}
$pluginService->updatePlugin($plugin);
try {
$pluginService->updatePlugin($plugin);
$this->info('Plugin updated.');
$this->info('Plugin updated.');
} catch (Exception $exception) {
$this->error('Could not update plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -12,12 +13,12 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel
case Successful = 'successful';
case Failed = 'failed';
public function getIcon(): string
public function getIcon(): BackedEnum
{
return match ($this) {
self::InProgress => 'tabler-circle-dashed',
self::Successful => 'tabler-circle-check',
self::Failed => 'tabler-circle-x',
self::InProgress => TablerIcon::CircleDashed,
self::Successful => TablerIcon::CircleCheck,
self::Failed => TablerIcon::CircleX,
};
}

View File

@@ -2,6 +2,7 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -23,20 +24,20 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
// HTTP Based
case Missing = 'missing';
public function getIcon(): string
public function getIcon(): BackedEnum
{
return match ($this) {
self::Created => 'tabler-heart-plus',
self::Starting => 'tabler-heart-up',
self::Running => 'tabler-heartbeat',
self::Restarting => 'tabler-heart-bolt',
self::Exited => 'tabler-heart-exclamation',
self::Paused => 'tabler-heart-pause',
self::Dead, self::Offline => 'tabler-heart-x',
self::Removing => 'tabler-heart-down',
self::Missing => 'tabler-heart-search',
self::Stopping => 'tabler-heart-minus',
self::Created => TablerIcon::HeartPlus,
self::Starting => TablerIcon::HeartUp,
self::Running => TablerIcon::Heartbeat,
self::Restarting => TablerIcon::HeartBolt,
self::Exited => TablerIcon::HeartExclamation,
self::Paused => TablerIcon::HeartPause,
self::Dead, self::Offline => TablerIcon::HeartX,
self::Removing => TablerIcon::HeartDown,
self::Missing => TablerIcon::HeartSearch,
self::Stopping => TablerIcon::HeartMinus,
};
}

View File

@@ -11,6 +11,9 @@ enum CustomizationKey: string
case TopNavigation = 'top_navigation';
case DashboardLayout = 'dashboard_layout';
case ButtonStyle = 'button_style';
case RedirectToAdmin = 'redirect_to_admin';
public function getDefaultValue(): string|int|bool
{
return match ($this) {
@@ -20,6 +23,8 @@ enum CustomizationKey: string
self::ConsoleGraphPeriod => 30,
self::TopNavigation => config('panel.filament.default-navigation', 'sidebar'),
self::DashboardLayout => 'grid',
self::ButtonStyle => true,
self::RedirectToAdmin => false,
};
}

View File

@@ -2,6 +2,7 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -11,12 +12,12 @@ enum PluginCategory: string implements HasIcon, HasLabel
case Theme = 'theme';
case Language = 'language';
public function getIcon(): string
public function getIcon(): BackedEnum
{
return match ($this) {
self::Plugin => 'tabler-package',
self::Theme => 'tabler-palette',
self::Language => 'tabler-language',
self::Plugin => TablerIcon::Package,
self::Theme => TablerIcon::Palette,
self::Language => TablerIcon::Language,
};
}

View File

@@ -2,6 +2,7 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -14,14 +15,14 @@ enum PluginStatus: string implements HasColor, HasIcon, HasLabel
case Errored = 'errored';
case Incompatible = 'incompatible';
public function getIcon(): string
public function getIcon(): BackedEnum
{
return match ($this) {
self::NotInstalled => 'tabler-heart-off',
self::Disabled => 'tabler-heart-x',
self::Enabled => 'tabler-heart-check',
self::Errored => 'tabler-heart-broken',
self::Incompatible => 'tabler-heart-cancel',
self::NotInstalled => TablerIcon::HeartOff,
self::Disabled => TablerIcon::HeartX,
self::Enabled => TablerIcon::HeartCheck,
self::Errored => TablerIcon::HeartBroken,
self::Incompatible => TablerIcon::HeartCancel,
};
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Enums;
use App\Models\Server;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\RateLimiter;
use Webmozart\Assert\Assert;
/**
* A basic resource throttler for individual servers. This is applied in addition
* to existing rate limits and allows the code to slow down speedy users that might
* be creating resources a little too quickly for comfort. This throttle generally
* only applies to creation flows, and not general view/edit/delete flows.
*/
enum ResourceLimit: string
{
case Websocket = 'websocket';
case AllocationCreate = 'allocation-create';
case BackupRestore = 'backup-restore';
case DatabaseCreate = 'database-create';
case ScheduleCreate = 'schedule-create';
case SubuserCreate = 'subuser-create';
case FilePull = 'file-pull';
public function throttleKey(): string
{
return "api.client:server-resource:{$this->name}";
}
/**
* Returns a middleware that will throttle the specific resource by server. This
* throttle applies to any user making changes to that resource on the specific
* server, it is NOT per-user.
*/
public function middleware(): string
{
return ThrottleRequests::using($this->throttleKey());
}
public function limit(): Limit
{
return match ($this) {
self::Websocket => Limit::perMinute(5),
self::BackupRestore => Limit::perMinutes(15, 3),
self::DatabaseCreate => Limit::perMinute(2),
self::SubuserCreate => Limit::perMinutes(15, 10),
self::FilePull => Limit::perMinutes(10, 5),
default => Limit::perMinute(2),
};
}
public static function boot(): void
{
foreach (self::cases() as $case) {
RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);
return $case->limit()->by($server->uuid);
});
}
}
}

View File

@@ -5,6 +5,8 @@ namespace App\Enums;
enum RolePermissionModels: string
{
case ApiKey = 'apiKey';
case Allocation = 'allocation';
case BackupHost = 'backupHost';
case DatabaseHost = 'databaseHost';
case Database = 'database';
case Egg = 'egg';
@@ -34,4 +36,9 @@ enum RolePermissionModels: string
{
return RolePermissionPrefixes::Update->value . ' ' . $this->value;
}
public function delete(): string
{
return RolePermissionPrefixes::Delete->value . ' ' . $this->value;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -14,14 +15,13 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
case Suspended = 'suspended';
case RestoringBackup = 'restoring_backup';
public function getIcon(): string
public function getIcon(): BackedEnum
{
return match ($this) {
self::Installing => 'tabler-heart-bolt',
self::InstallFailed => 'tabler-heart-x',
self::ReinstallFailed => 'tabler-heart-x',
self::Suspended => 'tabler-heart-cancel',
self::RestoringBackup => 'tabler-heart-up',
self::Installing => TablerIcon::HeartBolt,
self::InstallFailed, self::ReinstallFailed => TablerIcon::HeartX,
self::Suspended => TablerIcon::HeartCancel,
self::RestoringBackup => TablerIcon::HeartUp,
};
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum StepPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

@@ -2,6 +2,8 @@
namespace App\Enums;
use BackedEnum;
enum SubuserPermission: string
{
case WebsocketConnect = 'websocket.connect';
@@ -48,6 +50,9 @@ enum SubuserPermission: string
case ActivityRead = 'activity.read';
case MountRead = 'mount.read';
case MountUpdate = 'mount.update';
case StartupRead = 'startup.read';
case StartupUpdate = 'startup.update';
case StartupDockerImage = 'startup.docker-image';
@@ -55,6 +60,7 @@ enum SubuserPermission: string
case SettingsRename = 'settings.rename';
case SettingsDescription = 'settings.description';
case SettingsReinstall = 'settings.reinstall';
case SettingsChangeIcon = 'settings.change-icon';
/** @return string[] */
public function split(): array
@@ -67,21 +73,22 @@ enum SubuserPermission: string
return $this === self::WebsocketConnect;
}
public function getIcon(): ?string
public function getIcon(): ?BackedEnum
{
[$group, $permission] = $this->split();
return match ($group) {
'control' => 'tabler-terminal-2',
'user' => 'tabler-users',
'file' => 'tabler-files',
'backup' => 'tabler-file-zip',
'allocation' => 'tabler-network',
'startup' => 'tabler-player-play',
'database' => 'tabler-database',
'schedule' => 'tabler-clock',
'settings' => 'tabler-settings',
'activity' => 'tabler-stack',
'control' => TablerIcon::Terminal2,
'user' => TablerIcon::Users,
'file' => TablerIcon::Files,
'backup' => TablerIcon::FileZip,
'allocation' => TablerIcon::Network,
'startup' => TablerIcon::PlayerPlay,
'database' => TablerIcon::Database,
'schedule' => TablerIcon::Clock,
'settings' => TablerIcon::Settings,
'activity' => TablerIcon::Stack,
'mount' => TablerIcon::LayersLinked,
default => null,
};
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum TabPosition: string
{
case Before = 'before';
case After = 'after';
}

5997
app/Enums/TablerIcon.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
@@ -24,11 +25,11 @@ enum WebhookType: string implements HasColor, HasIcon, HasLabel
};
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return match ($this) {
self::Regular => 'tabler-world-www',
self::Discord => 'tabler-brand-discord',
self::Regular => TablerIcon::WorldWww,
self::Discord => TablerIcon::BrandDiscord,
};
}
}

View File

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

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Events\User;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
final class PasswordChanged
{
use Dispatchable;
public function __construct(public readonly User $user) {}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Exceptions;
use App\Exceptions\Solutions\ManifestDoesNotExistSolution;
use Exception;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
class ManifestDoesNotExistException extends Exception implements ProvidesSolution
{
public function getSolution(): Solution
{
return new ManifestDoesNotExistSolution();
}
}

View File

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

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Exceptions\Solutions;
use Spatie\Ignition\Contracts\Solution;
class ManifestDoesNotExistSolution implements Solution
{
public function getSolutionTitle(): string
{
return "The manifest.json file hasn't been generated yet";
}
public function getSolutionDescription(): string
{
return 'Run yarn run build:production to build the frontend first.';
}
public function getDocumentationLinks(): array
{
return [
'Docs' => 'https://github.com/pelican/panel/blob/master/package.json',
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Extensions\BackupAdapter;
use App\Models\Backup;
use App\Models\User;
use Filament\Schemas\Components\Component;
interface BackupAdapterSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function createBackup(Backup $backup): void;
public function deleteBackup(Backup $backup): void;
public function getDownloadLink(Backup $backup, User $user): string;
/** @return Component[] */
public function getConfigurationForm(): array;
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Extensions\BackupAdapter;
class BackupAdapterService
{
/** @var array<string, BackupAdapterSchemaInterface> */
private array $schemas = [];
/** @return BackupAdapterSchemaInterface[] */
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?BackupAdapterSchemaInterface
{
return array_get($this->schemas, $id);
}
public function register(BackupAdapterSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/** @return array<string, string> */
public function getMappings(): array
{
return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all();
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Extensions\BackupAdapter\Schemas;
use App\Extensions\BackupAdapter\BackupAdapterSchemaInterface;
use Illuminate\Support\Str;
abstract class BackupAdapterSchema implements BackupAdapterSchemaInterface
{
public function getName(): string
{
return Str::title($this->getId());
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace App\Extensions\BackupAdapter\Schemas;
use App\Enums\TablerIcon;
use App\Http\Controllers\Api\Remote\Backups\BackupRemoteUploadController;
use App\Models\Backup;
use App\Models\BackupHost;
use App\Models\User;
use App\Repositories\Daemon\DaemonBackupRepository;
use Aws\S3\S3Client;
use Carbon\CarbonImmutable;
use Exception;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Illuminate\Support\Arr;
final class S3BackupSchema extends BackupAdapterSchema
{
public function __construct(private readonly DaemonBackupRepository $repository) {}
private function createClient(BackupHost $backupHost): S3Client
{
$config = $backupHost->configuration;
$config['version'] = 'latest';
if (!empty($config['key']) && !empty($config['secret'])) {
$config['credentials'] = Arr::only($config, ['key', 'secret', 'token']);
}
return new S3Client($config);
}
public function getId(): string
{
return 's3';
}
public function createBackup(Backup $backup): void
{
$this->repository->setServer($backup->server)->create($backup);
}
public function deleteBackup(Backup $backup): void
{
$client = $this->createClient($backup->backupHost);
$client->deleteObject([
'Bucket' => $backup->backupHost->configuration['bucket'],
'Key' => "{$backup->server->uuid}/$backup->uuid.tar.gz",
]);
}
public function getDownloadLink(Backup $backup, User $user): string
{
$client = $this->createClient($backup->backupHost);
$request = $client->createPresignedRequest(
$client->getCommand('GetObject', [
'Bucket' => $backup->backupHost->configuration['bucket'],
'Key' => "{$backup->server->uuid}/$backup->uuid.tar.gz",
'ContentType' => 'application/x-gzip',
]),
CarbonImmutable::now()->addMinutes(5)
);
return $request->getUri()->__toString();
}
/** @return Component[] */
public function getConfigurationForm(): array
{
return [
TextInput::make('configuration.region')
->label(trans('admin/setting.backup.s3.default_region'))
->required(),
TextInput::make('configuration.key')
->label(trans('admin/setting.backup.s3.access_key'))
->required(),
TextInput::make('configuration.secret')
->label(trans('admin/setting.backup.s3.secret_key'))
->required(),
TextInput::make('configuration.bucket')
->label(trans('admin/setting.backup.s3.bucket'))
->required(),
TextInput::make('configuration.endpoint')
->label(trans('admin/setting.backup.s3.endpoint'))
->required(),
Toggle::make('configuration.use_path_style_endpoint')
->label(trans('admin/setting.backup.s3.use_path_style_endpoint'))
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
->stateCast(new BooleanStateCast(false)),
];
}
/** @return array{parts: string[], part_size: int} */
public function getUploadParts(Backup $backup, int $size): array
{
$expires = CarbonImmutable::now()->addMinutes(config('backups.presigned_url_lifespan', 60));
// Params for generating the presigned urls
$params = [
'Bucket' => $backup->backupHost->configuration['bucket'],
'Key' => "{$backup->server->uuid}/$backup->uuid.tar.gz",
'ContentType' => 'application/x-gzip',
];
$storageClass = $backup->backupHost->configuration['storage_class'];
if (!is_null($storageClass)) {
$params['StorageClass'] = $storageClass;
}
$client = $this->createClient($backup->backupHost);
// Execute the CreateMultipartUpload request
$result = $client->execute($client->getCommand('CreateMultipartUpload', $params));
// Get the UploadId from the CreateMultipartUpload request, this is needed to create
// the other presigned urls.
$params['UploadId'] = $result->get('UploadId');
// Retrieve configured part size
$maxPartSize = config('backups.max_part_size', BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE);
if ($maxPartSize <= 0) {
$maxPartSize = BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE;
}
// Create as many UploadPart presigned urls as needed
$parts = [];
for ($i = 0; $i < ($size / $maxPartSize); $i++) {
$parts[] = $client->createPresignedRequest(
$client->getCommand('UploadPart', array_merge($params, ['PartNumber' => $i + 1])),
$expires
)->getUri()->__toString();
}
// Set the upload_id on the backup in the database.
$backup->update(['upload_id' => $params['UploadId']]);
return [
'parts' => $parts,
'part_size' => $maxPartSize,
];
}
/**
* Marks a multipart upload in a given S3-compatible instance as failed or successful for the given backup.
*
* @param ?array<array{int, etag: string, part_number: string}> $parts
*
* @throws Exception
*/
public function completeMultipartUpload(Backup $backup, bool $successful, ?array $parts): void
{
// This should never really happen, but if it does don't let us fall victim to Amazon's
// wildly fun error messaging. Just stop the process right here.
if (empty($backup->upload_id)) {
// A failed backup doesn't need to error here, this can happen if the backup encounters
// an error before we even start the upload. AWS gives you tooling to clear these failed
// multipart uploads as needed too.
if (!$successful) {
return;
}
throw new Exception('Cannot complete backup request: no upload_id present on model.');
}
$params = [
'Bucket' => $backup->backupHost->configuration['bucket'],
'Key' => "{$backup->server->uuid}/$backup->uuid.tar.gz",
'UploadId' => $backup->upload_id,
];
$client = $this->createClient($backup->backupHost);
if (!$successful) {
$client->execute($client->getCommand('AbortMultipartUpload', $params));
return;
}
// Otherwise send a CompleteMultipartUpload request.
$params['MultipartUpload'] = [
'Parts' => [],
];
if (is_null($parts)) {
$params['MultipartUpload']['Parts'] = $client->execute($client->getCommand('ListParts', $params))['Parts'];
} else {
foreach ($parts as $part) {
$params['MultipartUpload']['Parts'][] = [
'ETag' => $part['etag'],
'PartNumber' => $part['part_number'],
];
}
}
$client->execute($client->getCommand('CompleteMultipartUpload', $params));
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Extensions\BackupAdapter\Schemas;
use App\Models\Backup;
use App\Models\User;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Services\Nodes\NodeJWTService;
use Carbon\CarbonImmutable;
use Exception;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Component;
use Illuminate\Http\Response;
final class WingsBackupSchema extends BackupAdapterSchema
{
public function __construct(private readonly DaemonBackupRepository $repository, private readonly NodeJWTService $jwtService) {}
public function getId(): string
{
return 'wings';
}
public function createBackup(Backup $backup): void
{
$this->repository->setServer($backup->server)->create($backup);
}
/** @throws Exception */
public function deleteBackup(Backup $backup): void
{
try {
$this->repository->setServer($backup->server)->delete($backup);
} catch (Exception $exception) {
// Don't fail the request if the Daemon responds with a 404, just assume the backup
// doesn't actually exist and remove its reference from the Panel as well.
if ($exception->getCode() !== Response::HTTP_NOT_FOUND) {
throw $exception;
}
}
}
public function getDownloadLink(Backup $backup, User $user): string
{
$token = $this->jwtService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setUser($user)
->setClaims([
'backup_uuid' => $backup->uuid,
'server_uuid' => $backup->server->uuid,
])
->handle($backup->server->node, $user->id . $backup->server->uuid);
return $backup->server->node->getConnectionAddress() . '/download/backup?token=' . $token->toString();
}
/** @return Component[] */
public function getConfigurationForm(): array
{
return [
TextEntry::make(trans('admin/backuphost.no_configuration')),
];
}
}

View File

@@ -1,177 +0,0 @@
<?php
namespace App\Extensions\Backups;
use App\Extensions\Filesystem\S3Filesystem;
use Aws\S3\S3Client;
use Closure;
use Illuminate\Foundation\Application;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use InvalidArgumentException;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
use Webmozart\Assert\Assert;
class BackupManager
{
/**
* The array of resolved backup drivers.
*
* @var array<string, FilesystemAdapter>
*/
protected array $adapters = [];
/**
* The registered custom driver creators.
*
* @var array<string, callable>
*/
protected array $customCreators;
public function __construct(protected Application $app) {}
/**
* Returns a backup adapter instance.
*/
public function adapter(?string $name = null): FilesystemAdapter
{
return $this->get($name ?: $this->getDefaultAdapter());
}
/**
* Set the given backup adapter instance.
*/
public function set(string $name, FilesystemAdapter $disk): self
{
$this->adapters[$name] = $disk;
return $this;
}
/**
* Gets a backup adapter.
*/
protected function get(string $name): FilesystemAdapter
{
return $this->adapters[$name] = $this->resolve($name);
}
/**
* Resolve the given backup disk.
*/
protected function resolve(string $name): FilesystemAdapter
{
$config = $this->getConfig($name);
if (empty($config['adapter'])) {
throw new InvalidArgumentException("Backup disk [$name] does not have a configured adapter.");
}
$adapter = $config['adapter'];
if (isset($this->customCreators[$name])) {
return $this->callCustomCreator($config);
}
$adapterMethod = 'create' . Str::studly($adapter) . 'Adapter';
if (method_exists($this, $adapterMethod)) {
$instance = $this->{$adapterMethod}($config);
Assert::isInstanceOf($instance, FilesystemAdapter::class);
return $instance;
}
throw new InvalidArgumentException("Adapter [$adapter] is not supported.");
}
/**
* Calls a custom creator for a given adapter type.
*
* @param array{adapter: string} $config
*/
protected function callCustomCreator(array $config): mixed
{
return $this->customCreators[$config['adapter']]($this->app, $config);
}
/**
* Creates a new daemon adapter.
*
* @param array<string, string> $config
*/
public function createWingsAdapter(array $config): FilesystemAdapter
{
return new InMemoryFilesystemAdapter();
}
/**
* Creates a new S3 adapter.
*
* @param array<string, string> $config
*/
public function createS3Adapter(array $config): FilesystemAdapter
{
$config['version'] = 'latest';
if (!empty($config['key']) && !empty($config['secret'])) {
$config['credentials'] = Arr::only($config, ['key', 'secret', 'token']);
}
$client = new S3Client($config);
return new S3Filesystem($client, $config['bucket'], $config['prefix'] ?? '', $config['options'] ?? []);
}
/**
* Returns the configuration associated with a given backup type.
*
* @return array<mixed>
*/
protected function getConfig(string $name): array
{
return config("backups.disks.$name") ?: [];
}
/**
* Get the default backup driver name.
*/
public function getDefaultAdapter(): string
{
return config('backups.default');
}
/**
* Set the default session driver name.
*/
public function setDefaultAdapter(string $name): void
{
config()->set('backups.default', $name);
}
/**
* Unset the given adapter instances.
*
* @param string|string[] $adapter
*/
public function forget(array|string $adapter): self
{
$adapters = &$this->adapters;
foreach ((array) $adapter as $adapterName) {
unset($adapters[$adapterName]);
}
return $this;
}
/**
* Register a custom adapter creator closure.
*/
public function extend(string $adapter, Closure $callback): self
{
$this->customCreators[$adapter] = $callback;
return $this;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Extensions\Captcha\Schemas;
use BackedEnum;
use Filament\Schemas\Components\Component;
interface CaptchaSchemaInterface
@@ -24,7 +25,7 @@ interface CaptchaSchemaInterface
*/
public function getSettingsForm(): array;
public function getIcon(): ?string;
public function getIcon(): null|string|BackedEnum;
public function validateResponse(?string $captchaResponse = null): void;
}

View File

@@ -2,8 +2,10 @@
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Enums\TablerIcon;
use App\Extensions\Captcha\Schemas\BaseSchema;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use BackedEnum;
use Exception;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
@@ -49,8 +51,8 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
->label(trans('admin/setting.captcha.verify'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
@@ -61,9 +63,9 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
]);
}
public function getIcon(): ?string
public function getIcon(): BackedEnum
{
return 'tabler-brand-cloudflare';
return TablerIcon::BrandCloudflare;
}
/**

View File

@@ -3,6 +3,7 @@
namespace App\Extensions\Features\Schemas;
use App\Enums\SubuserPermission;
use App\Enums\TablerIcon;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity;
use App\Models\Server;
@@ -73,7 +74,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
}
},
])
->hintIcon('tabler-code', fn () => implode('|', $serverVariable->variable->rules))
->hintIcon(TablerIcon::Code, fn () => implode('|', $serverVariable->variable->rules))
->label(fn () => $serverVariable->variable->name)
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),

View File

@@ -2,6 +2,7 @@
namespace App\Extensions\Features\Schemas;
use App\Enums\TablerIcon;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action;
use Illuminate\Support\Facades\Blade;
@@ -31,7 +32,7 @@ class PIDLimitSchema implements FeatureSchemaInterface
{
return Action::make($this->getId())
->requiresConfirmation()
->icon('tabler-alert-triangle')
->icon(TablerIcon::AlertTriangle)
->modalHeading(fn () => user()?->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML'

View File

@@ -1,38 +0,0 @@
<?php
namespace App\Extensions\Filesystem;
use Aws\S3\S3ClientInterface;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
class S3Filesystem extends AwsS3V3Adapter
{
/**
* @param array<mixed> $options
*/
public function __construct(
private S3ClientInterface $client,
private string $bucket,
string $prefix = '',
array $options = [],
) {
parent::__construct(
$client,
$bucket,
$prefix,
null,
null,
$options,
);
}
public function getClient(): S3ClientInterface
{
return $this->client;
}
public function getBucket(): string
{
return $this->bucket;
}
}

View File

@@ -2,8 +2,11 @@
namespace App\Extensions\OAuth;
use App\Models\User;
use BackedEnum;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Wizard\Step;
use Laravel\Socialite\Contracts\User as OAuthUser;
interface OAuthSchemaInterface
{
@@ -27,13 +30,13 @@ interface OAuthSchemaInterface
/** @return Step[] */
public function getSetupSteps(): array;
public function getIcon(): ?string;
public function getIcon(): null|string|BackedEnum;
public function getHexColor(): ?string;
public function isEnabled(): bool;
public function shouldCreateMissingUsers(): bool;
public function shouldCreateMissingUser(OAuthUser $user): bool;
public function shouldLinkMissingUsers(): bool;
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool;
}

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -33,9 +35,9 @@ final class BitbucketSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-bitbucket-f';
return TablerIcon::BrandBitbucketFilled;
}
public function getHexColor(): string

View File

@@ -2,13 +2,15 @@
namespace App\Extensions\OAuth\Schemas;
use BackedEnum;
final class CommonSchema extends OAuthSchema
{
public function __construct(
private readonly string $id,
private readonly ?string $name = null,
private readonly ?string $configName = null,
private readonly ?string $icon = null,
private readonly null|string|BackedEnum $icon = null,
private readonly ?string $hexColor = null,
) {}
@@ -27,7 +29,7 @@ final class CommonSchema extends OAuthSchema
return $this->configName ?? parent::getConfigKey();
}
public function getIcon(): ?string
public function getIcon(): null|string|BackedEnum
{
return $this->icon;
}

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -42,9 +44,9 @@ final class DiscordSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-discord-f';
return TablerIcon::BrandDiscordFilled;
}
public function getHexColor(): string

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -36,9 +38,9 @@ final class FacebookSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-facebook-f';
return TablerIcon::BrandFacebookFilled;
}
public function getHexColor(): string

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -18,11 +20,11 @@ final class GithubSchema extends OAuthSchema
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
Step::make('Register new GitHub OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">GitHub Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
@@ -42,9 +44,9 @@ final class GithubSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-github-f';
return TablerIcon::BrandGithubFilled;
}
public function getHexColor(): string

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -53,9 +55,9 @@ final class GitlabSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-gitlab';
return TablerIcon::BrandGitlab;
}
public function getHexColor(): string

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -42,9 +44,9 @@ final class GoogleSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-google-f';
return TablerIcon::BrandGoogleFilled;
}
public function getHexColor(): string

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -33,9 +35,9 @@ final class LinkedinSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-linkedin-f';
return TablerIcon::BrandLinkedinFilled;
}
public function getHexColor(): string

View File

@@ -2,13 +2,17 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use App\Extensions\OAuth\OAuthSchemaInterface;
use App\Models\User;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as OAuthUser;
abstract class OAuthSchema implements OAuthSchemaInterface
{
@@ -59,8 +63,8 @@ abstract class OAuthSchema implements OAuthSchemaInterface
->label(trans('admin/setting.oauth.create_missing_users'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
@@ -70,8 +74,8 @@ abstract class OAuthSchema implements OAuthSchemaInterface
->label(trans('admin/setting.oauth.link_missing_users'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
@@ -104,7 +108,7 @@ abstract class OAuthSchema implements OAuthSchemaInterface
return "OAUTH_{$id}_ENABLED";
}
public function getIcon(): ?string
public function getIcon(): null|string|BackedEnum
{
return null;
}
@@ -116,19 +120,17 @@ abstract class OAuthSchema implements OAuthSchemaInterface
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false);
return env($this->getConfigKey(), false);
}
public function shouldCreateMissingUsers(): bool
public function shouldCreateMissingUser(OAuthUser $user): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUsers(): bool
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool
{
$id = Str::upper($this->getId());

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -33,9 +35,9 @@ final class SlackSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-slack';
return TablerIcon::BrandSlack;
}
public function getHexColor(): string

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -59,9 +61,9 @@ final class SteamSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-steam-f';
return TablerIcon::BrandSteamFilled;
}
public function getHexColor(): string

View File

@@ -2,6 +2,8 @@
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
@@ -42,9 +44,9 @@ final class XSchema extends OAuthSchema
], parent::getSetupSteps());
}
public function getIcon(): string
public function getIcon(): BackedEnum
{
return 'tabler-brand-x';
return TablerIcon::BrandX;
}
public function getHexColor(): string

View File

@@ -8,7 +8,7 @@ use App\Services\Backups\InitiateBackupService;
final class CreateBackupSchema extends TaskSchema
{
public function __construct(private InitiateBackupService $backupService) {}
public function __construct(private InitiateBackupService $initiateService) {}
public function getId(): string
{
@@ -17,7 +17,7 @@ final class CreateBackupSchema extends TaskSchema
public function runTask(Task $task): void
{
$this->backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($task->server, null, true);
$this->initiateService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($task->server, null, true);
}
public function canCreate(Schedule $schedule): bool

View File

@@ -2,12 +2,14 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use App\Services\Helpers\SoftwareVersionService;
use BackedEnum;
use Filament\Pages\Dashboard as BaseDashboard;
class Dashboard extends BaseDashboard
{
protected static string|\BackedEnum|null $navigationIcon = 'tabler-layout-dashboard';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::LayoutDashboard;
private SoftwareVersionService $softwareVersionService;

View File

@@ -2,11 +2,12 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use BackedEnum;
use Carbon\Carbon;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Artisan;
use Spatie\Health\Commands\RunHealthChecksCommand;
use Spatie\Health\Enums\Status;
@@ -14,7 +15,7 @@ use Spatie\Health\ResultStores\ResultStore;
class Health extends Page
{
protected static string|\BackedEnum|null $navigationIcon = 'tabler-heart';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Heart;
protected string $view = 'filament.pages.health';
@@ -47,9 +48,9 @@ class Health extends Page
{
return [
Action::make('refresh')
->label(trans('admin/health.refresh'))
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-refresh')
->hiddenLabel()
->tooltip(trans('admin/health.refresh'))
->icon(TablerIcon::Refresh)
->action('refresh'),
];
}
@@ -128,16 +129,16 @@ class Health extends Page
return trans('admin/health.checks.failed', ['checks' => implode(', ', $failedNames)]);
}
public static function getNavigationIcon(): string
public static function getNavigationIcon(): BackedEnum
{
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
$results = app(ResultStore::class)->latestResults();
if ($results === null) {
return 'tabler-heart-question';
return TablerIcon::HeartQuestion;
}
return $results->containsFailingCheck() ? 'tabler-heart-exclamation' : 'tabler-heart-check';
return $results->containsFailingCheck() ? TablerIcon::HeartExclamation : TablerIcon::HeartCheck;
}
public function backgroundColor(string $str): string
@@ -162,14 +163,14 @@ class Health extends Page
};
}
public function icon(string $str): string
public function icon(string $str): BackedEnum
{
return match ($str) {
Status::ok()->value => 'tabler-circle-check',
Status::warning()->value => 'tabler-exclamation-circle',
Status::skipped()->value => 'tabler-circle-chevron-right',
Status::failed()->value, Status::crashed()->value => 'tabler-circle-x',
default => 'tabler-help-circle'
Status::ok()->value => TablerIcon::CircleCheck,
Status::warning()->value => TablerIcon::ExclamationCircle,
Status::skipped()->value => TablerIcon::CircleChevronRight,
Status::failed()->value, Status::crashed()->value => TablerIcon::CircleX,
default => TablerIcon::HelpCircle
};
}
}

View File

@@ -2,24 +2,26 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Actions\ViewLogAction;
use Boquizo\FilamentLogViewer\Pages\ListLogs as BaseListLogs;
use Boquizo\FilamentLogViewer\Tables\Columns\LevelColumn;
use Boquizo\FilamentLogViewer\Tables\Columns\NameColumn;
use Boquizo\FilamentLogViewer\UseCases\ParseDateUseCase;
use Boquizo\FilamentLogViewer\Utils\Level;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Facades\Http;
class ListLogs extends BaseListLogs
{
protected string $view = 'filament.components.list-logs';
public function getHeading(): string|null|\Illuminate\Contracts\Support\Htmlable
public function getHeading(): string|null|Htmlable
{
return trans('admin/log.navigation.panel_logs');
}
@@ -28,7 +30,7 @@ class ListLogs extends BaseListLogs
{
return parent::table($table)
->emptyStateHeading(trans('admin/log.empty_table'))
->emptyStateIcon('tabler-check')
->emptyStateIcon(TablerIcon::Check)
->columns([
NameColumn::make('date'),
LevelColumn::make(Level::ALL)
@@ -46,17 +48,21 @@ class ListLogs extends BaseListLogs
])
->recordActions([
ViewLogAction::make()
->icon('tabler-file-description')->iconSize(IconSize::Large)->iconButton(),
->icon(TablerIcon::FileDescription)->iconButton(),
DownloadAction::make()
->icon('tabler-file-download')->iconSize(IconSize::Large)->iconButton(),
->tooltip(fn ($record) => trans('filament-log-viewer::log.table.actions.download.label', ['log' => ParseDateUseCase::execute($record['date'])]))
->icon(TablerIcon::FileDownload)->iconButton(),
Action::make('uploadLogs')
->hiddenLabel()
->icon('tabler-world-upload')->iconSize(IconSize::Large)->iconButton()
->tooltip(trans('admin/log.actions.upload_tooltip', ['url' => 'logs.pelican.dev']))
->icon(TablerIcon::WorldUpload)
->requiresConfirmation()
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn ($record) => trans('admin/log.actions.upload_logs_description', ['file' => $record['date'], 'url' => 'https://logs.pelican.dev']))
->action(function ($record) {
$logPath = storage_path('logs/' . $record['date']);
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
$extension = config('filament-log-viewer.pattern.extension', '.log');
$logPath = storage_path('logs/' . $prefix . $record['date'] . $extension);
if (!file_exists($logPath)) {
Notification::make()
@@ -73,18 +79,12 @@ class ListLogs extends BaseListLogs
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
$response = Http::timeout(10)
->asMultipart()
->attach('c', $content)
->attach('e', '14d')
->post('https://logs.pelican.dev');
if ($response->failed()) {
Notification::make()
@@ -104,7 +104,7 @@ class ListLogs extends BaseListLogs
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
Action::make('exclude_viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
@@ -123,7 +123,7 @@ class ListLogs extends BaseListLogs
}
}),
DeleteAction::make()
->iconSize(IconSize::Medium)->iconButton(),
->icon(TablerIcon::Trash)->iconButton(),
]);
}
}

View File

@@ -2,15 +2,18 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use App\Extensions\Avatar\AvatarService;
use App\Extensions\Captcha\CaptchaService;
use App\Extensions\OAuth\OAuthService;
use App\Models\Backup;
use App\Notifications\MailTested;
use App\Traits\EnvironmentWriterTrait;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use BackedEnum;
use BladeUI\Icons\Exceptions\SvgNotFound;
use BladeUI\Icons\Factory as IconFactory;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@@ -36,7 +39,6 @@ use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\Width;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\Arr;
@@ -53,10 +55,11 @@ class Settings extends Page implements HasSchemas
CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions;
}
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
use EnvironmentWriterTrait;
use InteractsWithForms;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-settings';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Settings;
protected string $view = 'filament.pages.settings';
@@ -66,6 +69,8 @@ class Settings extends Page implements HasSchemas
protected CaptchaService $captchaService;
protected IconFactory $iconFactory;
/** @var array<mixed>|null */
public ?array $data = [];
@@ -74,11 +79,12 @@ class Settings extends Page implements HasSchemas
$this->form->fill();
}
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService): void
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService, IconFactory $iconFactory): void
{
$this->oauthService = $oauthService;
$this->avatarService = $avatarService;
$this->captchaService = $captchaService;
$this->iconFactory = $iconFactory;
}
public static function canAccess(): bool
@@ -96,11 +102,7 @@ class Settings extends Page implements HasSchemas
return trans('admin/setting.title');
}
/**
* @return array<Component>
*
* @throws Exception
*/
/** @return array<Component> */
protected function getFormSchema(): array
{
return [
@@ -108,34 +110,44 @@ class Settings extends Page implements HasSchemas
->columns()
->persistTabInQueryString()
->disabled(fn () => !user()?->can('update settings'))
->tabs([
Tab::make('general')
->label(trans('admin/setting.navigation.general'))
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('captcha')
->label(trans('admin/setting.navigation.captcha'))
->icon('tabler-shield')
->schema($this->captchaSettings())
->columns(1),
Tab::make('mail')
->label(trans('admin/setting.navigation.mail'))
->icon('tabler-mail')
->schema($this->mailSettings()),
Tab::make('backup')
->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth')
->schema($this->oauthSettings())
->columns(1),
Tab::make('misc')
->label(trans('admin/setting.navigation.misc'))
->icon('tabler-tool')
->schema($this->miscSettings()),
]),
->tabs($this->getTabs()),
];
}
/**
* @return Tab[]
*
* @throws Exception
*/
protected function getDefaultTabs(): array
{
return [
Tab::make('general')
->label(trans('admin/setting.navigation.general'))
->icon(TablerIcon::Home)
->schema($this->generalSettings()),
Tab::make('captcha')
->label(trans('admin/setting.navigation.captcha'))
->icon(TablerIcon::Shield)
->schema($this->captchaSettings())
->columns(1),
Tab::make('mail')
->label(trans('admin/setting.navigation.mail'))
->icon(TablerIcon::Mail)
->schema($this->mailSettings()),
Tab::make('backup')
->label(trans('admin/setting.navigation.backup'))
->icon(TablerIcon::Box)
->schema($this->backupSettings()),
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon(TablerIcon::BrandOauth)
->schema($this->oauthSettings())
->columns(1),
Tab::make('misc')
->label(trans('admin/setting.navigation.misc'))
->icon(TablerIcon::Tool)
->schema($this->miscSettings()),
];
}
@@ -154,12 +166,12 @@ class Settings extends Page implements HasSchemas
->schema([
TextInput::make('APP_LOGO')
->label(trans('admin/setting.general.app_logo'))
->hintIcon('tabler-question-mark', trans('admin/setting.general.app_logo_help'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/setting.general.app_logo_help'))
->default(env('APP_LOGO'))
->placeholder('/pelican.svg'),
TextInput::make('APP_FAVICON')
->label(trans('admin/setting.general.app_favicon'))
->hintIcon('tabler-question-mark', trans('admin/setting.general.app_favicon_help'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/setting.general.app_favicon_help'))
->required()
->default(env('APP_FAVICON', '/pelican.ico'))
->placeholder('/pelican.ico'),
@@ -170,8 +182,8 @@ class Settings extends Page implements HasSchemas
Toggle::make('APP_DEBUG')
->label(trans('admin/setting.general.debug_mode'))
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->stateCast(new BooleanStateCast(false))
@@ -188,8 +200,8 @@ class Settings extends Page implements HasSchemas
Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
->label(trans('admin/setting.general.uploadable_avatars'))
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->stateCast(new BooleanStateCast(false))
@@ -236,16 +248,16 @@ class Settings extends Page implements HasSchemas
->placeholder(trans('admin/setting.general.trusted_proxies_help'))
->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies')))))
->hintActions([
Action::make('clear')
Action::make('hint_clear')
->label(trans('admin/setting.general.clear'))
->color('danger')
->icon('tabler-trash')
->icon(TablerIcon::Trash)
->requiresConfirmation()
->authorize(fn () => user()?->can('update settings'))
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
Action::make('cloudflare')
Action::make('hint_cloudflare')
->label(trans('admin/setting.general.set_to_cf'))
->icon('tabler-brand-cloudflare')
->icon(TablerIcon::BrandCloudflare)
->authorize(fn () => user()?->can('update settings'))
->action(function (Factory $client, Set $set) {
$ips = collect();
@@ -256,7 +268,7 @@ class Settings extends Page implements HasSchemas
->connectTimeout(3)
->get('https://api.cloudflare.com/client/v4/ips');
if ($response->getStatusCode() === 200) {
if ($response->status() === 200) {
$result = $response->json('result');
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
$ips->push(...data_get($result, $value));
@@ -287,7 +299,7 @@ class Settings extends Page implements HasSchemas
$formFields[] = Section::make($schema->getName())
->columns(5)
->icon($schema->getIcon() ?? 'tabler-shield')
->icon($schema->getIcon() ?? TablerIcon::Shield)
->collapsed(fn () => !$schema->isEnabled())
->collapsible()
->schema([
@@ -341,9 +353,9 @@ class Settings extends Page implements HasSchemas
->live()
->default(env('MAIL_MAILER', config('mail.default')))
->hintAction(
Action::make('test')
Action::make('hint_test')
->label(trans('admin/setting.mail.test_mail'))
->icon('tabler-send')
->icon(TablerIcon::Send)
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
->authorize(fn () => user()?->can('update settings'))
->action(function (Get $get) {
@@ -477,16 +489,6 @@ class Settings extends Page implements HasSchemas
private function backupSettings(): array
{
return [
ToggleButtons::make('APP_BACKUP_DRIVER')
->label(trans('admin/setting.backup.backup_driver'))
->columnSpanFull()
->inline()
->options([
Backup::ADAPTER_DAEMON => 'Wings',
Backup::ADAPTER_AWS_S3 => 'S3',
])
->live()
->default(env('APP_BACKUP_DRIVER', config('backups.default'))),
Section::make(trans('admin/setting.backup.throttle'))
->description(trans('admin/setting.backup.throttle_help'))
->columns()
@@ -506,41 +508,6 @@ class Settings extends Page implements HasSchemas
->suffix('Seconds')
->default(config('backups.throttles.period')),
]),
Section::make(trans('admin/setting.backup.s3.s3_title'))
->columns()
->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3)
->schema([
TextInput::make('AWS_DEFAULT_REGION')
->label(trans('admin/setting.backup.s3.default_region'))
->required()
->default(config('backups.disks.s3.region')),
TextInput::make('AWS_ACCESS_KEY_ID')
->label(trans('admin/setting.backup.s3.access_key'))
->required()
->default(config('backups.disks.s3.key')),
TextInput::make('AWS_SECRET_ACCESS_KEY')
->label(trans('admin/setting.backup.s3.secret_key'))
->required()
->default(config('backups.disks.s3.secret')),
TextInput::make('AWS_BACKUPS_BUCKET')
->label(trans('admin/setting.backup.s3.bucket'))
->required()
->default(config('backups.disks.s3.bucket')),
TextInput::make('AWS_ENDPOINT')
->label(trans('admin/setting.backup.s3.endpoint'))
->required()
->default(config('backups.disks.s3.endpoint')),
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
->label(trans('admin/setting.backup.s3.use_path_style_endpoint'))
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->stateCast(new BooleanStateCast(false))
->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))),
]),
];
}
@@ -555,26 +522,34 @@ class Settings extends Page implements HasSchemas
$oauthSchemas = $this->oauthService->getAll();
foreach ($oauthSchemas as $schema) {
$id = Str::upper($schema->getId());
$key = $schema->getConfigKey();
$icon = $schema->getIcon();
if (is_string($icon)) {
try {
$this->iconFactory->svg($icon);
} catch (SvgNotFound) {
$icon = null;
}
}
$formFields[] = Section::make($schema->getName())
->columns(5)
->icon($schema->getIcon() ?? 'tabler-brand-oauth')
->collapsed(fn () => !env($key, false))
->icon($icon ?? TablerIcon::BrandOauth)
->collapsed(fn () => !$schema->isEnabled())
->collapsible()
->schema([
Hidden::make($key)
->live()
->default(env($key)),
->default($schema->isEnabled()),
Actions::make([
Action::make("disable_oauth_$id")
Action::make('disable_oauth_' . $schema->getId())
->visible(fn (Get $get) => $get($key))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.oauth.disable'))
->color('danger')
->action(fn (Set $set) => $set($key, false)),
Action::make("enable_oauth_$id")
Action::make('enable_oauth_' . $schema->getId())
->visible(fn (Get $get) => !$get($key))
->disabled(fn () => !user()?->can('update settings'))
->label(trans('admin/setting.oauth.enable'))
@@ -619,8 +594,8 @@ class Settings extends Page implements HasSchemas
->schema([
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
->label(trans('admin/setting.misc.auto_allocation.question'))
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
@@ -630,8 +605,8 @@ class Settings extends Page implements HasSchemas
Toggle::make('PANEL_CLIENT_ALLOCATIONS_CREATE_NEW')
->label(trans('admin/setting.misc.auto_allocation.create_new'))
->helperText(trans('admin/setting.misc.auto_allocation.create_new_help'))
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
@@ -664,8 +639,8 @@ class Settings extends Page implements HasSchemas
->schema([
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.server_installed'))
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
@@ -674,8 +649,8 @@ class Settings extends Page implements HasSchemas
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.server_reinstalled'))
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
@@ -723,8 +698,8 @@ class Settings extends Page implements HasSchemas
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
->label(trans('admin/setting.misc.activity_log.log_admin'))
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
@@ -760,8 +735,8 @@ class Settings extends Page implements HasSchemas
->schema([
Toggle::make('PANEL_EDITABLE_SERVER_DESCRIPTIONS')
->label(trans('admin/setting.misc.server.edit_server_desc'))
->onIcon('tabler-check')
->offIcon('tabler-x')
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
@@ -823,7 +798,6 @@ class Settings extends Page implements HasSchemas
$this->writeToEnvironment($data);
Artisan::call('config:clear');
Artisan::call('queue:restart');
$this->redirect($this->getUrl());
@@ -846,9 +820,10 @@ class Settings extends Page implements HasSchemas
{
return [
Action::make('save')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy')
->hiddenLabel()
->icon(TablerIcon::DeviceFloppy)
->action('save')
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->authorize(fn () => user()?->can('update settings'))
->keyBindings(['mod+s']),
];

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Pages;
use App\Enums\TablerIcon;
use App\Traits\ResolvesRecordDate;
use Boquizo\FilamentLogViewer\Actions\BackAction;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
@@ -9,7 +10,6 @@ use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Pages\ViewLog as BaseViewLog;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Http;
class ViewLogs extends BaseViewLog
@@ -20,20 +20,23 @@ class ViewLogs extends BaseViewLog
{
return [
BackAction::make()
->icon('tabler-arrow-left')->iconSize(IconSize::ExtraLarge)->iconButton(),
->tooltip(trans('filament-log-viewer::log.table.actions.close.label'))
->icon(TablerIcon::ArrowLeft)->iconButton(),
DeleteAction::make(withTooltip: true)
->iconSize(IconSize::ExtraLarge)->iconButton(),
->icon(TablerIcon::Trash)->iconButton(),
DownloadAction::make(withTooltip: true)
->icon('tabler-file-download')->iconSize(IconSize::ExtraLarge)->iconButton(),
->icon(TablerIcon::FileDownload)->iconButton(),
Action::make('uploadLogs')
->hiddenLabel()
->icon('tabler-world-upload')->iconSize(IconSize::ExtraLarge)->iconButton()
->icon(TablerIcon::WorldUpload)
->requiresConfirmation()
->tooltip(trans('admin/log.actions.upload_tooltip', ['url' => 'logs.pelican.dev']))
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn () => trans('admin/log.actions.upload_logs_description', ['file' => $this->resolveRecordDate(), 'url' => 'https://logs.pelican.dev']))
->action(function () {
$logPath = storage_path('logs/' . $this->resolveRecordDate());
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
$extension = config('filament-log-viewer.pattern.extension', '.log');
$logPath = storage_path('logs/' . $prefix . $this->resolveRecordDate() . $extension);
if (!file_exists($logPath)) {
Notification::make()
@@ -50,18 +53,12 @@ class ViewLogs extends BaseViewLog
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
$response = Http::timeout(10)
->asMultipart()
->attach('c', $content)
->attach('e', '14d')
->post('https://logs.pelican.dev');
if ($response->failed()) {
Notification::make()
@@ -81,7 +78,7 @@ class ViewLogs extends BaseViewLog
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
Action::make('exclude_viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\ApiKeys;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\ApiKeys\Pages\CreateApiKey;
use App\Filament\Admin\Resources\ApiKeys\Pages\ListApiKeys;
use App\Filament\Admin\Resources\Users\Pages\EditUser;
@@ -11,7 +12,9 @@ use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
@@ -19,8 +22,8 @@ use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
@@ -34,7 +37,7 @@ class ApiKeyResource extends Resource
protected static ?string $model = ApiKey::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-key';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Key;
public static function getNavigationLabel(): string
{
@@ -77,7 +80,7 @@ class ApiKeyResource extends Resource
->columns([
TextColumn::make('key')
->label(trans('admin/apikey.table.key'))
->icon('tabler-clipboard-text')
->icon(TablerIcon::ClipboardText)
->state(fn (ApiKey $key) => $key->identifier . $key->token)
->copyable(),
TextColumn::make('memo')
@@ -96,10 +99,12 @@ class ApiKeyResource extends Resource
->url(fn (ApiKey $apiKey) => user()?->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
])
->recordActions([
DeleteAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
DeleteAction::make(),
])
->emptyStateIcon('tabler-key')
->toolbarActions([
CreateAction::make(),
])
->emptyStateIcon(TablerIcon::Key)
->emptyStateDescription('')
->emptyStateHeading(trans('admin/apikey.empty'));
}
@@ -109,12 +114,44 @@ class ApiKeyResource extends Resource
*/
public static function defaultForm(Schema $schema): Schema
{
$permissionList = ApiKey::getPermissionList();
return $schema
->components([
Section::make(trans('admin/apikey.permissions.all'))
->description(trans('admin/apikey.permissions.all_description'))
->columnSpanFull()
->schema([
ToggleButtons::make('permissions_all')
->hiddenLabel()
->inline()
->options([
0 => trans('admin/apikey.permissions.none'),
1 => trans('admin/apikey.permissions.read'),
3 => trans('admin/apikey.permissions.read_write'),
])
->icons([
0 => TablerIcon::BookOff,
1 => TablerIcon::Book,
3 => TablerIcon::Writing,
])
->colors([
0 => 'success',
1 => 'warning',
3 => 'danger',
])
->live()
->afterStateUpdated(function ($state, callable $set) use ($permissionList) {
foreach ($permissionList as $resource) {
$set('permissions_' . $resource, $state);
}
})
->default(0),
]),
Fieldset::make('Permissions')
->columnSpanFull()
->schema(
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
collect($permissionList)->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
->label(str($resource)->replace('_', ' ')->title())->inline()
->options([
0 => trans('admin/apikey.permissions.none'),
@@ -122,9 +159,9 @@ class ApiKeyResource extends Resource
3 => trans('admin/apikey.permissions.read_write'),
])
->icons([
0 => 'tabler-book-off',
1 => 'tabler-book',
3 => 'tabler-writing',
0 => TablerIcon::BookOff,
1 => TablerIcon::Book,
3 => TablerIcon::Writing,
])
->colors([
0 => 'success',

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\ApiKeys\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\ApiKeys\ApiKeyResource;
use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions;
@@ -9,7 +10,6 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@@ -26,9 +26,12 @@ class CreateApiKey extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
Action::make('create')
->hiddenLabel()
->action('create')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->icon(TablerIcon::FilePlus),
];
}

View File

@@ -5,11 +5,7 @@ namespace App\Filament\Admin\Resources\ApiKeys\Pages;
use App\Filament\Admin\Resources\ApiKeys\ApiKeyResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListApiKeys extends ListRecords
{
@@ -17,14 +13,4 @@ class ListApiKeys extends ListRecords
use CanCustomizeHeaderWidgets;
protected static string $resource = ApiKeyResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace App\Filament\Admin\Resources\BackupHosts;
use App\Enums\TablerIcon;
use App\Extensions\BackupAdapter\BackupAdapterService;
use App\Filament\Admin\Resources\BackupHosts\Pages\CreateBackupHost;
use App\Filament\Admin\Resources\BackupHosts\Pages\EditBackupHost;
use App\Filament\Admin\Resources\BackupHosts\Pages\ListBackupHosts;
use App\Filament\Admin\Resources\BackupHosts\Pages\ViewBackupHost;
use App\Filament\Admin\Resources\BackupHosts\RelationManagers\BackupsRelationManager;
use App\Models\BackupHost;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class BackupHostResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = BackupHost::class;
protected static string|BackedEnum|null $navigationIcon = TablerIcon::FileZip;
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationBadge(): ?string
{
return (string) static::getEloquentQuery()->count() ?: null;
}
public static function getNavigationLabel(): string
{
return static::getPluralModelLabel();
}
public static function getModelLabel(): string
{
return trans_choice('admin/backuphost.model_label', 1);
}
public static function getPluralModelLabel(): string
{
return trans_choice('admin/backuphost.model_label', 2);
}
public static function getNavigationGroup(): ?string
{
return trans('admin/dashboard.advanced');
}
/** @throws Exception */
public static function defaultTable(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(trans('admin/backuphost.name')),
TextColumn::make('schema')
->label(trans('admin/backuphost.schema'))
->badge(),
TextColumn::make('backups_count')
->counts('backups')
->label(trans('admin/backuphost.backups')),
TextColumn::make('nodes.name')
->badge()
->placeholder(trans('admin/backuphost.all_nodes')),
])
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
])
->toolbarActions([
CreateAction::make(),
])
->emptyStateIcon(TablerIcon::FileZip)
->emptyStateDescription(trans('admin/backuphost.local_backups_only'))
->emptyStateHeading(trans('admin/backuphost.no_backup_hosts'));
}
/** @throws Exception */
public static function defaultForm(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label(trans('admin/backuphost.name'))
->required(),
Select::make('schema')
->label(trans('admin/backuphost.schema'))
->required()
->selectablePlaceholder(false)
->searchable()
->options(fn (BackupAdapterService $service) => $service->getMappings())
->live(onBlur: true),
Select::make('node_ids')
->label(trans('admin/backuphost.linked_nodes'))
->multiple()
->searchable()
->preload()
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id'))),
Section::make(trans('admin/backuphost.configuration'))
->columnSpanFull()
->columns()
->schema(function (?BackupHost $backupHost, Get $get, BackupAdapterService $service) {
$schema = $get('schema') ?? $backupHost?->schema;
if (!$schema) {
return [];
}
$schema = $service->get($schema);
if ($schema) {
return $schema->getConfigurationForm();
}
return [];
}),
]);
}
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
BackupsRelationManager::class,
];
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => ListBackupHosts::route('/'),
'create' => CreateBackupHost::route('/create'),
'view' => ViewBackupHost::route('/{record}'),
'edit' => EditBackupHost::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Filament\Admin\Resources\BackupHosts\Pages;
use App\Filament\Admin\Resources\BackupHosts\BackupHostResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Resources\Pages\CreateRecord;
class CreateBackupHost extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = BackupHostResource::class;
protected static bool $canCreateAnother = false;
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Filament\Admin\Resources\BackupHosts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\BackupHosts\BackupHostResource;
use App\Models\BackupHost;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditBackupHost extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = BackupHostResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make()
->label(fn (BackupHost $backupHost) => $backupHost->backups()->count() > 0 ? trans('admin/backuphost.delete_help') : trans('filament-actions::delete.single.modal.actions.delete.label'))
->disabled(fn (BackupHost $backupHost) => $backupHost->backups()->count() > 0)
->hidden(fn () => BackupHost::count() === 1),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Filament\Admin\Resources\BackupHosts\Pages;
use App\Filament\Admin\Resources\BackupHosts\BackupHostResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Resources\Pages\ListRecords;
class ListBackupHosts extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = BackupHostResource::class;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Filament\Admin\Resources\BackupHosts\Pages;
use App\Filament\Admin\Resources\BackupHosts\BackupHostResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewBackupHost extends ViewRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = BackupHostResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Filament\Admin\Resources\BackupHosts\RelationManagers;
use App\Enums\TablerIcon;
use App\Filament\Components\Tables\Columns\BytesColumn;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class BackupsRelationManager extends RelationManager
{
protected static string $relationship = 'backups';
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('name')
->heading(null)
->columns([
TextColumn::make('name')
->label(trans('server/backup.actions.create.name'))
->searchable(),
BytesColumn::make('bytes')
->label(trans('server/backup.size')),
DateTimeColumn::make('created_at')
->label(trans('server/backup.created_at'))
->since()
->sortable(),
TextColumn::make('status')
->label(trans('server/backup.status'))
->badge(),
IconColumn::make('is_locked')
->label(trans('server/backup.is_locked'))
->visibleFrom('md')
->trueIcon(TablerIcon::Lock)
->falseIcon(TablerIcon::LockOpen),
]);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\DatabaseHosts;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\CreateDatabaseHost;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\EditDatabaseHost;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\ListDatabaseHosts;
@@ -12,7 +13,10 @@ use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
@@ -37,7 +41,7 @@ class DatabaseHostResource extends Resource
protected static ?string $model = DatabaseHost::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-database';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Database;
protected static ?string $recordTitleAttribute = 'name';
@@ -94,10 +98,13 @@ class DatabaseHostResource extends Resource
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
])
->groupedBulkActions([
DeleteBulkAction::make(),
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->emptyStateIcon('tabler-database')
->emptyStateIcon(TablerIcon::Database)
->emptyStateDescription('')
->emptyStateHeading(trans('admin/databasehost.no_database_hosts'));
}

View File

@@ -2,11 +2,13 @@
namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
@@ -44,6 +46,17 @@ class CreateDatabaseHost extends CreateRecord
$this->service = $service;
}
protected function getCreateFormAction(): Action
{
$hasFormWrapper = $this->hasFormWrapper();
return Action::make('exclude_create')
->label(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit($hasFormWrapper ? $this->getSubmitFormLivewireMethodName() : null)
->action($hasFormWrapper ? null : $this->getSubmitFormLivewireMethodName())
->keyBindings(['mod+s']);
}
/** @return Step[]
* @throws Exception
*/
@@ -175,7 +188,7 @@ class CreateDatabaseHost extends CreateRecord
->title(trans('admin/databasehost.error'))
->body($exception->getMessage())
->color('danger')
->icon('tabler-database')
->icon(TablerIcon::Database)
->danger()
->send();

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Models\DatabaseHost;
use App\Services\Databases\Hosts\HostUpdateService;
@@ -12,7 +13,6 @@ use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Model;
use PDOException;
@@ -36,12 +36,14 @@ class EditDatabaseHost extends EditRecord
{
return [
DeleteAction::make()
->label(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? trans('admin/databasehost.delete_help') : trans('filament-actions::delete.single.modal.actions.delete.label'))
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0)
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
->tooltip(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0 ? trans('admin/databasehost.delete_help') : trans('filament-actions::delete.single.modal.actions.delete.label'))
->disabled(fn (DatabaseHost $databaseHost) => $databaseHost->databases()->count() > 0),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
];
}
@@ -63,7 +65,7 @@ class EditDatabaseHost extends EditRecord
->title(trans('admin/databasehost.error'))
->body($exception->getMessage())
->color('danger')
->icon('tabler-database')
->icon(TablerIcon::Database)
->danger()
->send();

View File

@@ -5,11 +5,7 @@ namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListDatabaseHosts extends ListRecords
{
@@ -17,14 +13,4 @@ class ListDatabaseHosts extends ListRecords
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseHostResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -9,7 +9,6 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewDatabaseHost extends ViewRecord
{
@@ -22,8 +21,7 @@ class ViewDatabaseHost extends ViewRecord
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
EditAction::make(),
];
}
}

View File

@@ -10,7 +10,6 @@ use Filament\Actions\ViewAction;
use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -37,7 +36,7 @@ class DatabasesRelationManager extends RelationManager
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
TextInput::make('max_connections')
->label(trans('admin/databasehost.table.max_connections'))
->formatStateUsing(fn (Database $record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
->formatStateUsing(fn (Database $record) => $record->max_connections ?: trans('admin/databasehost.unlimited')),
TextInput::make('jdbc')
->label(trans('admin/databasehost.table.connection_string'))
->columnSpanFull()
@@ -63,15 +62,14 @@ class DatabasesRelationManager extends RelationManager
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
TextColumn::make('max_connections')
->label(trans('admin/databasehost.table.max_connections'))
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? trans('admin/databasehost.unlimited') : $record->max_connections),
->formatStateUsing(fn ($record) => $record->max_connections ?: trans('server/database.unlimited')),
DateTimeColumn::make('created_at')
->label(trans('admin/databasehost.table.created_at')),
])
->recordActions([
ViewAction::make()
->color('primary'),
DeleteAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
DeleteAction::make(),
]);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Eggs;
use App\Enums\CustomizationKey;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\Pages\CreateEgg;
use App\Filament\Admin\Resources\Eggs\Pages\EditEgg;
use App\Filament\Admin\Resources\Eggs\Pages\ListEggs;
@@ -10,6 +11,7 @@ use App\Filament\Admin\Resources\Eggs\RelationManagers\ServersRelationManager;
use App\Models\Egg;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use BackedEnum;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
@@ -21,7 +23,7 @@ class EggResource extends Resource
protected static ?string $model = Egg::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-eggs';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Eggs;
protected static ?string $recordTitleAttribute = 'name';

View File

@@ -3,13 +3,14 @@
namespace App\Filament\Admin\Resources\Eggs\Pages;
use App\Enums\EditorLanguages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Filament\Components\Forms\Fields\MonacoEditor;
use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use App\Traits\Filament\CanCustomizeTabs;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Checkbox;
@@ -28,7 +29,6 @@ use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Unique;
@@ -37,6 +37,7 @@ class CreateEgg extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
protected static string $resource = EggResource::class;
@@ -46,9 +47,12 @@ class CreateEgg extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
Action::make('create')
->hiddenLabel()
->action('create')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->icon(TablerIcon::FilePlus),
];
}
@@ -57,227 +61,232 @@ class CreateEgg extends CreateRecord
return [];
}
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
Tabs::make()
->tabs($this->getTabs())
->columnSpanFull()
->persistTabInQueryString(),
]);
}
/** @return Tab[] */
protected function getDefaultTabs(): array
{
return [
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->maxLength(255)
->required()
->email()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.author_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(2)
->columnSpanFull()
->helperText(trans('admin/egg.description_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->keyPlaceholder('Default')
->valueLabel(trans('admin/egg.startup_command'))
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->label(trans('admin/egg.force_ip'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->default(1),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->keyPlaceholder('Java 21')
->valueLabel(trans('admin/egg.docker_uri'))
->valuePlaceholder('ghcr.io/pelican-eggs/yolks:java_21')
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->required()
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->default('{}')
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->default('{}')
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
->hiddenLabel()
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->orderColumn()
->reorderAction(fn (Action $action) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->maxLength(255)
->required()
->email()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.author_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(2)
->columnSpanFull()
->helperText(trans('admin/egg.description_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->keyPlaceholder('Default')
->valueLabel(trans('admin/egg.startup_command'))
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->default(1),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->keyPlaceholder('Java 21')
->valueLabel(trans('admin/egg.docker_uri'))
->valuePlaceholder('ghcr.io/pelican-eggs/yolks:java_21')
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->required()
->debounce(750)
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->default('{}')
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->default('{}')
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
->hiddenLabel()
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->reorderable()->orderColumn()
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
]),
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->default('bash')
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))
->language(EditorLanguages::shell)
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon(TablerIcon::Code, fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->lazy(),
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
]),
])->columnSpanFull()->persistTabInQueryString(),
]);
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->default('bash')
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))
->language(EditorLanguages::shell)
->columnSpanFull()
->lazy(),
]),
];
}
protected function handleRecordCreation(array $data): Model

View File

@@ -3,21 +3,23 @@
namespace App\Filament\Admin\Resources\Eggs\Pages;
use App\Enums\EditorLanguages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Actions\DeleteIcon;
use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction;
use App\Filament\Components\Actions\UploadIcon;
use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Filament\Components\Forms\Fields\MonacoEditor;
use App\Models\Egg;
use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use App\Traits\Filament\CanCustomizeTabs;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
@@ -26,11 +28,8 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Flex;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Tabs;
@@ -38,400 +37,260 @@ use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rules\Unique;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class EditEgg extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
protected static string $resource = EggResource::class;
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon('tabler-egg')
Tabs::make()
->tabs($this->getTabs())
->columnSpanFull()
->persistTabInQueryString(),
]);
}
/** @return Tab[] */
protected function getDefaultTabs(): array
{
return [
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon(TablerIcon::Egg)
->schema([
Grid::make(2)
->columnStart(1)
->schema([
Grid::make(2)
->columnSpan(1)
->schema([
Image::make('', '')
->hidden(fn ($record) => !$record->image)
->url(fn ($record) => $record->image)
->alt('')
->alignJustify()
->imageSize(150)
->columnSpanFull(),
Flex::make([
Action::make('uploadImage')
->iconButton()
->iconSize(IconSize::Large)
->icon('tabler-photo-up')
->modal()
->modalHeading('')
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('imageUrl'),
Hidden::make('imageExtension'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
$set('imageUrl', null);
$set('imageExtension', null);
Image::make('', 'icon')
->hidden(fn ($record) => !$record->icon)
->url(fn ($record) => $record->icon)
->imageSize(150)
->columnSpanFull()
->alignJustify(),
UploadIcon::make(),
DeleteIcon::make()
->iconStoragePath(Egg::getIconStoragePath()),
]),
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
->helperText(trans('admin/egg.description_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->columnSpan(1)
->disabled(),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->valueLabel(trans('admin/egg.startup_command'))
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.update_url_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon(TablerIcon::ServerCog)
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->dehydrateStateUsing(fn ($state) => blank($state) ? '{}' : $state)
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon(TablerIcon::Variable)
->schema([
Repeater::make('variables')
->hiddenLabel()
->grid()
->relationship('variables')
->orderColumn()
->reorderAction(fn (Action $action) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
->collapsible()->collapsed()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return;
}
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
try {
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new Exception(trans('admin/egg.import.no_local_ip'));
}
$set('imageUrl', $state);
$set('imageExtension', $extension);
$set('image_url_error', null);
} catch (Exception $e) {
$set('image_url_error', $e->getMessage());
$set('imageUrl', null);
$set('imageExtension', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn ($get) => $get('image_url_error') !== null)
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->disk('public')
->directory(Egg::ICON_STORAGE_PATH)
->acceptedFileTypes([
'image/png',
'image/jpeg',
'image/webp',
'image/svg+xml',
])
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
return $record->uuid . '.' . $file->getClientOriginalExtension();
}),
]),
]),
])
->action(function (array $data, $record): void {
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
if (!empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
if (empty($data['imageUrl']) && empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('delete_image')
->visible(fn ($record) => $record->image)
->hiddenLabel()
->icon('tabler-trash')
->iconButton()
->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
Notification::make()
->title(trans('admin/egg.import.image_deleted'))
->success()
->send();
$record->refresh();
}),
]),
]),
return $data;
})
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
->helperText(trans('admin/egg.description_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->columnSpan(1)
->disabled(),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->valueLabel(trans('admin/egg.startup_command'))
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon('tabler-server-cog')
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->debounce(750)
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon('tabler-variable')
->schema([
Repeater::make('variables')
->hiddenLabel()
->grid()
->relationship('variables')
->reorderable()
->collapsible()->collapsed()
->orderColumn()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
]),
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon('tabler-file-download')
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
MonacoEditor::make('script_install')
->hiddenLabel()
->language(EditorLanguages::shell)
->columnSpanFull(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon(TablerIcon::Code, fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
]),
])->columnSpanFull()->persistTabInQueryString(),
]);
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon(TablerIcon::FileDownload)
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->hiddenLabel()
->language(EditorLanguages::shell)
->columnSpanFull(),
]),
];
}
/** @return array<Action|ActionGroup> */
@@ -440,14 +299,16 @@ class EditEgg extends EditRecord
return [
DeleteAction::make()
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use'))
->iconButton()->iconSize(IconSize::ExtraLarge),
->tooltip(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use')),
ExportEggAction::make(),
ImportEggAction::make()
->multiple(false),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
];
}
@@ -456,37 +317,6 @@ class EditEgg extends EditRecord
$this->fillForm();
}
/**
* Save an image from URL download to a file.
*
* @throws Exception
*/
private function saveImageFromUrl(string $imageUrl, string $extension, Egg $egg): void
{
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$data = @file_get_contents($imageUrl, false, $context, 0, 1048576); // 1024KB
if (empty($data)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$normalizedExtension = match ($extension) {
'svg+xml' => 'svg',
'jpeg' => 'jpg',
default => $extension,
};
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
}
protected function getFormActions(): array
{
return [];

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Eggs\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction;
@@ -12,17 +13,17 @@ use App\Models\Egg;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ReplicateAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class ListEggs extends ListRecords
@@ -37,6 +38,8 @@ class ListEggs extends ListRecords
*/
public function table(Table $table): Table
{
$defaultEggIcon = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')));
return $table
->searchable(true)
->defaultPaginationPageOption(25)
@@ -44,13 +47,11 @@ class ListEggs extends ListRecords
TextColumn::make('id')
->label('Id')
->hidden(),
ImageColumn::make('image')
ImageColumn::make('icon')
->label('')
->alignCenter()
->circular()
->getStateUsing(fn ($record) => $record->image
? $record->image
: 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')))),
->getStateUsing(fn (Egg $record) => $record->icon ?: $defaultEggIcon),
TextColumn::make('name')
->label(trans('admin/egg.name'))
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
@@ -63,19 +64,13 @@ class ListEggs extends ListRecords
])
->recordActions([
EditAction::make()
->iconButton()
->tooltip(trans('filament-actions::edit.single.label'))
->iconSize(IconSize::Large),
->tooltip(trans('filament-actions::edit.single.label')),
ExportEggAction::make()
->tooltip(trans('filament-actions::export.modal.actions.export.label'))
->iconSize(IconSize::Large),
->tooltip(trans('filament-actions::export.modal.actions.export.label')),
UpdateEggAction::make()
->tooltip(trans_choice('admin/egg.update', 1))
->iconSize(IconSize::Large),
->tooltip(trans_choice('admin/egg.update', 1)),
ReplicateAction::make()
->iconButton()
->tooltip(trans('filament-actions::replicate.single.label'))
->iconSize(IconSize::Large)
->modal(false)
->excludeAttributes(['author', 'uuid', 'update_url', 'servers_count', 'created_at', 'updated_at'])
->beforeReplicaSaved(function (Egg $replica) {
@@ -86,19 +81,54 @@ class ListEggs extends ListRecords
->after(fn (Egg $record, Egg $replica) => $record->variables->each(fn ($variable) => $variable->replicate()->fill(['egg_id' => $replica->id])->save()))
->successRedirectUrl(fn (Egg $replica) => EditEgg::getUrl(['record' => $replica])),
])
->groupedBulkActions([
DeleteBulkAction::make()
->before(fn (&$records) => $records = $records->filter(function ($egg) {
/** @var Egg $egg */
return $egg->servers_count <= 0;
})),
UpdateEggBulkAction::make()
->before(fn (&$records) => $records = $records->filter(function ($egg) {
/** @var Egg $egg */
return cache()->get("eggs.$egg->uuid.update", false);
})),
->toolbarActions([
ImportEggAction::make()
->multiple(),
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make('exclude_bulk_delete')
->before(function (Collection &$records) {
$eggsWithServers = $records->filter(fn (Egg $egg) => $egg->servers_count > 0);
if ($eggsWithServers->isNotEmpty()) {
$eggNames = $eggsWithServers->map(fn (Egg $egg) => sprintf('%s (%d server%s)', $egg->name, $egg->servers_count, $egg->servers_count > 1 ? 's' : ''))
->join(', ');
Notification::make()
->danger()
->title(trans('admin/egg.cannot_delete', ['count' => $eggsWithServers->count()]))
->body(trans('admin/egg.eggs_have_servers', ['eggs' => $eggNames]))
->send();
}
$records = $records->filter(fn (Egg $egg) => $egg->servers_count <= 0);
if ($records->isEmpty()) {
$this->halt();
}
}),
UpdateEggBulkAction::make('exclude_bulk_update')
->before(function (Collection &$records) {
$eggsWithoutUpdateUrl = $records->filter(fn (Egg $egg) => $egg->update_url === null);
if ($eggsWithoutUpdateUrl->isNotEmpty()) {
$eggNames = $eggsWithoutUpdateUrl->pluck('name')->join(', ');
Notification::make()
->warning()
->title(trans('admin/egg.cannot_update', ['count' => $eggsWithoutUpdateUrl->count()]))
->body(trans('admin/egg.no_update_url', ['eggs' => $eggNames]))
->send();
}
$records = $records->filter(fn (Egg $egg) => $egg->update_url !== null);
if ($records->isEmpty()) {
$this->halt();
}
}),
]),
])
->emptyStateIcon('tabler-eggs')
->emptyStateIcon(TablerIcon::Eggs)
->emptyStateDescription('')
->emptyStateHeading(trans('admin/egg.no_eggs'))
->filters([
@@ -106,18 +136,4 @@ class ListEggs extends ListRecords
->model(Egg::class),
]);
}
/** @return array<Action|ActionGroup>
* @throws Exception
*/
protected function getDefaultHeaderActions(): array
{
return [
ImportEggAction::make()
->multiple(),
CreateAction::make()
->icon('tabler-file-plus')
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Mounts;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Mounts\Pages\CreateMount;
use App\Filament\Admin\Resources\Mounts\Pages\EditMount;
use App\Filament\Admin\Resources\Mounts\Pages\ListMounts;
@@ -11,7 +12,10 @@ use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
@@ -21,7 +25,6 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Schema;
@@ -38,7 +41,7 @@ class MountResource extends Resource
protected static ?string $model = Mount::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-layers-linked';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::LayersLinked;
protected static ?string $recordTitleAttribute = 'name';
@@ -89,19 +92,28 @@ class MountResource extends Resource
TextColumn::make('read_only')
->label(trans('admin/mount.table.read_only'))
->badge()
->icon(fn ($state) => $state ? 'tabler-writing-off' : 'tabler-writing')
->icon(fn ($state) => $state ? TablerIcon::WritingOff : TablerIcon::Writing)
->color(fn ($state) => $state ? 'success' : 'warning')
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
TextColumn::make('user_mountable')
->label(trans('admin/mount.table.user_mountable'))
->badge()
->icon(fn ($state) => $state ? TablerIcon::User : TablerIcon::UserOff)
->color(fn ($state) => $state ? 'success' : 'warning')
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.user_mountable') : trans('admin/mount.toggles.not_user_mountable')),
])
->recordActions([
ViewAction::make()
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
])
->groupedBulkActions([
DeleteBulkAction::make(),
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->emptyStateIcon('tabler-layers-linked')
->emptyStateIcon(TablerIcon::LayersLinked)
->emptyStateDescription('')
->emptyStateHeading(trans('admin/mount.no_mounts'));
}
@@ -118,7 +130,8 @@ class MountResource extends Resource
->label(trans('admin/mount.name'))
->required()
->helperText(trans('admin/mount.name_help'))
->maxLength(64),
->maxLength(64)
->columnSpanFull(),
ToggleButtons::make('read_only')
->label(trans('admin/mount.read_only'))
->helperText(trans('admin/mount.read_only_help'))
@@ -128,8 +141,8 @@ class MountResource extends Resource
true => trans('admin/mount.toggles.read_only'),
])
->icons([
false => 'tabler-writing',
true => 'tabler-writing-off',
false => TablerIcon::Writing,
true => TablerIcon::WritingOff,
])
->colors([
false => 'warning',
@@ -137,6 +150,24 @@ class MountResource extends Resource
])
->inline()
->default(false),
ToggleButtons::make('user_mountable')
->label(trans('admin/mount.user_mountable'))
->helperText(trans('admin/mount.user_mountable_help'))
->stateCast(new BooleanStateCast(false, true))
->options([
false => trans('admin/mount.toggles.not_user_mountable'),
true => trans('admin/mount.toggles.user_mountable'),
])
->icons([
false => TablerIcon::UserOff,
true => TablerIcon::User,
])
->colors([
false => 'warning',
true => 'success',
])
->inline()
->default(true),
TextInput::make('source')
->label(trans('admin/mount.source'))
->required()
@@ -151,30 +182,32 @@ class MountResource extends Resource
->label(trans('admin/mount.description'))
->helperText(trans('admin/mount.description_help'))
->columnSpanFull(),
])->columnSpan(1)->columns([
'default' => 1,
'lg' => 2,
]),
Group::make()->schema([
Section::make()->schema([
Select::make('eggs')->multiple()
->label(trans('admin/mount.eggs'))
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
->preload(),
Select::make('nodes')->multiple()
->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
->searchable(['name', 'fqdn'])
->preload(),
])
->columnSpan([
'default' => 1,
'lg' => 2,
])
->columns([
'default' => 1,
'xl' => 2,
]),
])->columns([
'default' => 1,
'lg' => 2,
Section::make()->schema([
Select::make('eggs')
->multiple()
->label(trans('admin/mount.eggs'))
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
->preload(),
Select::make('nodes')
->multiple()
->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
->searchable(['name', 'fqdn'])
->preload(),
]),
])->columns([
'default' => 1,
'lg' => 2,
'lg' => 3,
]);
}

View File

@@ -2,13 +2,13 @@
namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@@ -25,9 +25,12 @@ class CreateMount extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
Action::make('create')
->hiddenLabel()
->action('create')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->icon(TablerIcon::FilePlus),
];
}
@@ -39,7 +42,6 @@ class CreateMount extends CreateRecord
protected function handleRecordCreation(array $data): Model
{
$data['uuid'] ??= Str::uuid()->toString();
$data['user_mountable'] = 1;
return parent::handleRecordCreation($data);
}

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
@@ -9,7 +10,6 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
class EditMount extends EditRecord
{
@@ -22,11 +22,13 @@ class EditMount extends EditRecord
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
DeleteAction::make(),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
];
}

View File

@@ -5,11 +5,7 @@ namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListMounts extends ListRecords
{
@@ -17,14 +13,4 @@ class ListMounts extends ListRecords
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -9,7 +9,6 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewMount extends ViewRecord
{
@@ -22,9 +21,7 @@ class ViewMount extends ViewRecord
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make()
->iconSize(IconSize::ExtraLarge)
->iconButton(),
EditAction::make(),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Nodes;
use App\Enums\CustomizationKey;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Nodes\Pages\CreateNode;
use App\Filament\Admin\Resources\Nodes\Pages\EditNode;
use App\Filament\Admin\Resources\Nodes\Pages\ListNodes;
@@ -11,6 +12,7 @@ use App\Filament\Admin\Resources\Nodes\RelationManagers\ServersRelationManager;
use App\Models\Node;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use BackedEnum;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
@@ -23,7 +25,7 @@ class NodeResource extends Resource
protected static ?string $model = Node::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-server-2';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Server2;
protected static ?string $recordTitleAttribute = 'name';

View File

@@ -2,11 +2,12 @@
namespace App\Filament\Admin\Resources\Nodes\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Models\Node;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use App\Traits\Filament\CanCustomizeSteps;
use Filament\Actions\Action;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TagsInput;
@@ -27,394 +28,406 @@ class CreateNode extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeSteps;
protected static string $resource = NodeResource::class;
protected static bool $canCreateAnother = false;
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Wizard::make([
Step::make('basic')
->label(trans('admin/node.tabs.basic_settings'))
->icon('tabler-server')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) {
if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.fqdn_help');
}
return '';
}
return trans('admin/node.error');
})
->hintColor('danger')
->hint(function ($state) {
if (is_ip($state) && request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
return '';
})
->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
}
})
->maxLength(255),
TextInput::make('ip')
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => trans('admin/node.valid'),
false => trans('admin/node.invalid'),
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
TextInput::make('daemon_connect')
->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer(),
TextInput::make('name')
->label(trans('admin/node.display_name'))
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection')
->label(trans('admin/node.ssl'))
->columnSpan(1)
->inline()
->helperText(function (Get $get) {
if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl'));
}
if (is_ip($get('fqdn'))) {
return trans('admin/node.ssl_help');
}
return '';
})
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([
'http' => 'HTTP',
'https' => 'HTTPS (SSL)',
'https_proxy' => 'HTTPS with (reverse) proxy',
])
->colors([
'http' => 'warning',
'https' => 'success',
'https_proxy' => 'success',
])
->icons([
'http' => 'tabler-lock-open-off',
'https' => 'tabler-lock',
'https_proxy' => 'tabler-shield-lock',
])
->default(fn () => request()->isSecure() ? 'https' : 'http')
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings'))
->icon('tabler-server-cog')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
ToggleButtons::make('maintenance_mode')
->label(trans('admin/node.maintenance_mode'))->inline()
->columnSpan(1)
->default(false)
->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help'))
->options([
true => trans('admin/node.enabled'),
false => trans('admin/node.disabled'),
])
->colors([
true => 'danger',
false => 'success',
]),
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label(trans('admin/node.use_for_deploy'))->inline()
->options([
true => trans('admin/node.yes'),
false => trans('admin/node.no'),
])
->colors([
true => 'success',
false => 'danger',
]),
TagsInput::make('tags')
->label(trans('admin/node.tags'))
->columnSpan(2),
TextInput::make('upload_size')
->label(trans('admin/node.upload_limit'))
->helperText(trans('admin/node.upload_limit_help.0'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.1'))
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->maxValue(1024)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label(trans('admin/node.sftp_port'))
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(2)
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_mem')
->dehydrated()
->label(trans('admin/node.memory'))->inlineLabel()->inline()
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label(trans('admin/node.memory_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label(trans('admin/node.overallocate'))->inlineLabel()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_disk')
->dehydrated()
->label(trans('admin/node.disk'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.disk_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->dehydrated()
->label(trans('admin/node.cpu'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.cpu_limit'))->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0)
->required(),
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%')
->required(),
]),
]),
])->columnSpanFull()
->nextAction(fn (Action $action) => $action->label(trans('admin/node.next_step'))->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-right'))
->previousAction(fn (Action $action) => $action->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-left'))
Wizard::make($this->getSteps())
->columnSpanFull()
->nextAction(fn (Action $action) => $action->tooltip(fn () => $action->getLabel())->iconButton()->iconSize(IconSize::ExtraLarge)->icon(TablerIcon::ArrowRight))
->previousAction(fn (Action $action) => $action->tooltip(fn () => $action->getLabel())->iconButton()->iconSize(IconSize::ExtraLarge)->icon(TablerIcon::ArrowLeft))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::icon-button
type="submit"
iconSize="xl"
icon="tabler-file-plus"
>
{{ trans('admin/node.create') }}
</x-filament::icon-button>
BLADE))),
<x-filament::icon-button
type="submit"
iconSize="xl"
icon="tabler-plus"
tooltip="{{ trans('admin/node.create') }}"
>
{{ trans('admin/node.create') }}
</x-filament::icon-button>
BLADE))),
]);
}
/** @return Step[] */
protected function getDefaultSteps(): array
{
return [
Step::make('basic')
->label(trans('admin/node.tabs.basic_settings'))
->icon(TablerIcon::Server)
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) {
if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.fqdn_help');
}
return '';
}
return trans('admin/node.error');
})
->hintColor('danger')
->hint(function ($state) {
if (is_ip($state) && request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
return '';
})
->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
}
})
->maxLength(255),
TextInput::make('ip')
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => trans('admin/node.valid'),
false => trans('admin/node.invalid'),
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
TextInput::make('daemon_connect')
->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer(),
TextInput::make('name')
->label(trans('admin/node.display_name'))
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection')
->label(trans('admin/node.ssl'))
->columnSpan(1)
->inline()
->helperText(function (Get $get) {
if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl'));
}
if (is_ip($get('fqdn'))) {
return trans('admin/node.ssl_help');
}
return '';
})
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([
'http' => 'HTTP',
'https' => 'HTTPS (SSL)',
'https_proxy' => 'HTTPS with (reverse) proxy',
])
->colors([
'http' => 'warning',
'https' => 'success',
'https_proxy' => 'success',
])
->icons([
'http' => TablerIcon::LockOpenOff,
'https' => TablerIcon::Lock,
'https_proxy' => TablerIcon::ShieldLock,
])
->default(fn () => request()->isSecure() ? 'https' : 'http')
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings'))
->icon(TablerIcon::ServerCog)
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
ToggleButtons::make('maintenance_mode')
->label(trans('admin/node.maintenance_mode'))->inline()
->columnSpan(1)
->default(false)
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.maintenance_mode_help'))
->options([
true => trans('admin/node.enabled'),
false => trans('admin/node.disabled'),
])
->colors([
true => 'danger',
false => 'success',
]),
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label(trans('admin/node.use_for_deploy'))->inline()
->options([
true => trans('admin/node.yes'),
false => trans('admin/node.no'),
])
->colors([
true => 'success',
false => 'danger',
]),
TagsInput::make('tags')
->label(trans('admin/node.tags'))
->columnSpan(2),
TextInput::make('upload_size')
->label(trans('admin/node.upload_limit'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.upload_limit_help'))
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_base')
->label(trans('admin/node.daemon_base'))
->placeholder('/var/lib/pelican/volumes')
->hintIcon(TablerIcon::QuestionMark, trans('admin/node.daemon_base_help'))
->columnSpan(1)
->required()
->default('/var/lib/pelican/volumes')
->rule('regex:/^([\/][\d\w.\-\/]+)$/'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label(trans('admin/node.sftp_port'))
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(1)
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_mem')
->dehydrated()
->label(trans('admin/node.memory'))->inlineLabel()->inline()
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label(trans('admin/node.memory_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label(trans('admin/node.overallocate'))->inlineLabel()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_disk')
->dehydrated()
->label(trans('admin/node.disk'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.disk_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->dehydrated()
->label(trans('admin/node.cpu'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.cpu_limit'))->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0)
->required(),
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%')
->required(),
]),
]),
];
}
protected function getRedirectUrlParameters(): array
{
return [

File diff suppressed because it is too large Load Diff

View File

@@ -2,18 +2,17 @@
namespace App\Filament\Admin\Resources\Nodes\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Filament\Components\Tables\Columns\NodeClientHealthColumn;
use App\Filament\Components\Tables\Columns\NodeHealthColumn;
use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Node;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -36,6 +35,7 @@ class ListNodes extends ListRecords
->searchable()
->hidden(),
NodeHealthColumn::make('health'),
NodeClientHealthColumn::make('reachable'),
TextColumn::make('name')
->label(trans('admin/node.table.name'))
->sortable()
@@ -48,14 +48,14 @@ class ListNodes extends ListRecords
IconColumn::make('scheme')
->visibleFrom('xl')
->label('SSL')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open-off')
->trueIcon(TablerIcon::Lock)
->falseIcon(TablerIcon::LockOpenOff)
->state(fn (Node $node) => $node->scheme === 'https'),
IconColumn::make('public')
->label(trans('admin/node.table.public'))
->visibleFrom('lg')
->trueIcon('tabler-eye-check')
->falseIcon('tabler-eye-cancel'),
->trueIcon(TablerIcon::EyeCheck)
->falseIcon(TablerIcon::EyeCancel),
TextColumn::make('servers_count')
->visibleFrom('sm')
->counts('servers')
@@ -65,7 +65,10 @@ class ListNodes extends ListRecords
->recordActions([
EditAction::make(),
])
->emptyStateIcon('tabler-server-2')
->toolbarActions([
CreateAction::make(),
])
->emptyStateIcon(TablerIcon::Server2)
->emptyStateDescription('')
->emptyStateHeading(trans('admin/node.no_nodes'))
->filters([
@@ -73,14 +76,4 @@ class ListNodes extends ListRecords
->model(Node::class),
]);
}
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-file-plus'),
];
}
}

View File

@@ -2,11 +2,13 @@
namespace App\Filament\Admin\Resources\Nodes\RelationManagers;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
use App\Filament\Components\Actions\UpdateNodeAllocations;
use App\Models\Allocation;
use App\Models\Node;
use App\Services\Allocations\AssignmentService;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\DeleteBulkAction;
@@ -16,7 +18,6 @@ use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
@@ -29,7 +30,7 @@ class AllocationsRelationManager extends RelationManager
{
protected static string $relationship = 'allocations';
protected static string|\BackedEnum|null $icon = 'tabler-plug-connected';
protected static string|BackedEnum|null $icon = TablerIcon::PlugConnected;
public function setTitle(): string
{
@@ -57,7 +58,7 @@ class AllocationsRelationManager extends RelationManager
->label(trans('admin/node.ports')),
TextColumn::make('server.name')
->label(trans('admin/node.table.servers'))
->icon('tabler-brand-docker')
->icon(TablerIcon::BrandDocker)
->visibleFrom('md')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
@@ -86,12 +87,14 @@ class AllocationsRelationManager extends RelationManager
DeleteBulkAction::make()
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
Action::make('create new allocation')
->label(trans('admin/node.create_allocation'))
->icon('tabler-world-plus')
->iconButton()->iconSize(IconSize::ExtraLarge)
->tooltip(trans('admin/node.create_allocation'))
->icon(TablerIcon::WorldPlus)
->schema(fn () => [
Select::make('allocation_ip')
->options(fn () => collect($this->getOwnerRecord()->ipAddresses())->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->options(fn (Get $get) => collect($this->getOwnerRecord()->ipAddresses())
->when($get('allocation_ip'), fn ($ips, $current) => $ips->push($current))
->unique()
->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->label(trans('admin/node.ip_address'))
->inlineLabel()
->ip()
@@ -99,14 +102,26 @@ class AllocationsRelationManager extends RelationManager
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
->live()
->hintAction(
Action::make('refresh')
->iconButton()
->icon('tabler-refresh')
Action::make('hint_refresh')
->hiddenLabel()
->icon(TablerIcon::Refresh)
->tooltip(trans('admin/node.refresh'))
->action(function () {
cache()->forget("nodes.{$this->getOwnerRecord()->id}.ips");
})
)
->suffixAction(
Action::make('custom_ip')
->icon(TablerIcon::Keyboard)
->tooltip(trans('admin/node.custom_ip'))
->schema([
TextInput::make('custom_ip')
->label(trans('admin/node.ip_address'))
->ip()
->required(),
])
->action(fn (array $data, Set $set) => $set('allocation_ip', $data['custom_ip']))
)
->required(),
TextInput::make('allocation_alias')
->label(trans('admin/node.table.alias'))

View File

@@ -3,7 +3,9 @@
namespace App\Filament\Admin\Resources\Nodes\RelationManagers;
use App\Enums\ServerResourceType;
use App\Enums\TablerIcon;
use App\Models\Server;
use BackedEnum;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
@@ -13,7 +15,7 @@ class ServersRelationManager extends RelationManager
{
protected static string $relationship = 'servers';
protected static string|\BackedEnum|null $icon = 'tabler-brand-docker';
protected static string|BackedEnum|null $icon = TablerIcon::BrandDocker;
public function setTitle(): string
{

View File

@@ -3,9 +3,14 @@
namespace App\Filament\Admin\Resources\Plugins;
use App\Enums\PluginStatus;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
use App\Jobs\Plugin\InstallPlugin;
use App\Jobs\Plugin\UninstallPlugin;
use App\Jobs\Plugin\UpdatePlugin;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@@ -14,7 +19,6 @@ use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Http\UploadedFile;
@@ -23,7 +27,7 @@ class PluginResource extends Resource
{
protected static ?string $model = Plugin::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-packages';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Packages;
protected static ?string $recordTitleAttribute = 'name';
@@ -59,7 +63,7 @@ class PluginResource extends Resource
TextColumn::make('name')
->label(trans('admin/plugin.name'))
->description(fn (Plugin $plugin) => (strlen($plugin->description) > 80) ? substr($plugin->description, 0, 80).'...' : $plugin->description)
->icon(fn (Plugin $plugin) => $plugin->isUpdateAvailable() ? 'tabler-versions-off' : 'tabler-versions')
->icon(fn (Plugin $plugin) => $plugin->isUpdateAvailable() ? TablerIcon::VersionsOff : TablerIcon::Versions)
->iconColor(fn (Plugin $plugin) => $plugin->isUpdateAvailable() ? 'danger' : 'success')
->tooltip(fn (Plugin $plugin) => $plugin->isUpdateAvailable() ? trans('admin/plugin.update_available') : null)
->sortable()
@@ -82,15 +86,15 @@ class PluginResource extends Resource
->sortable(),
])
->recordActions([
Action::make('view')
Action::make('exclude_view')
->label(trans('filament-actions::view.single.label'))
->icon(fn (Plugin $plugin) => $plugin->getReadme() ? 'tabler-eye' : 'tabler-eye-share')
->icon(fn (Plugin $plugin) => $plugin->getReadme() ? TablerIcon::Eye : TablerIcon::EyeShare)
->color('gray')
->visible(fn (Plugin $plugin) => $plugin->getReadme() || $plugin->url)
->url(fn (Plugin $plugin) => !$plugin->getReadme() ? $plugin->url : null, true)
->slideOver(true)
->modalHeading('Readme')
->modalSubmitAction(fn (Plugin $plugin) => Action::make('visit_website')
->modalSubmitAction(fn (Plugin $plugin) => Action::make('exclude_visit_website')
->label(trans('admin/plugin.visit_website'))
->visible(!is_null($plugin->url))
->url($plugin->url, true)
@@ -102,52 +106,66 @@ class PluginResource extends Resource
->markdown()
->state(fn (Plugin $plugin) => $plugin->getReadme()),
] : null),
Action::make('settings')
Action::make('exclude_settings')
->label(trans('admin/plugin.settings'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon('tabler-settings')
->icon(TablerIcon::Settings)
->color('primary')
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::Enabled && $plugin->hasSettings())
->schema(fn (Plugin $plugin) => $plugin->getSettingsForm())
->action(fn (array $data, Plugin $plugin) => $plugin->saveSettings($data))
->slideOver(),
ActionGroup::make([
Action::make('install')
Action::make('exclude_install')
->label(trans('admin/plugin.install'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon('tabler-terminal')
->icon(TablerIcon::Terminal)
->color('success')
->hidden(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->installPlugin($plugin, !$plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
->action(function (Plugin $plugin) {
try {
InstallPlugin::dispatch(user(), $plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.installed'))
->send();
Notification::make()
->success()
->title(trans('admin/plugin.notifications.install_started'))
->body(trans('admin/plugin.notifications.background_info'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.install_error'))
->body($exception->getMessage())
->send();
}
}),
Action::make('update')
Action::make('exclude_update')
->label(trans('admin/plugin.update'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon('tabler-download')
->icon(TablerIcon::Download)
->color('success')
->visible(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled && $plugin->isUpdateAvailable())
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->updatePlugin($plugin);
->action(function (Plugin $plugin) {
try {
UpdatePlugin::dispatch(user(), $plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.updated'))
->send();
Notification::make()
->success()
->title(trans('admin/plugin.notifications.update_started'))
->body(trans('admin/plugin.notifications.background_info'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.update_error'))
->body($exception->getMessage())
->send();
}
}),
Action::make('enable')
Action::make('exclude_enable')
->label(trans('admin/plugin.enable'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon('tabler-check')
->icon(TablerIcon::Check)
->color('success')
->visible(fn (Plugin $plugin) => $plugin->canEnable())
->requiresConfirmation(fn (Plugin $plugin, PluginService $pluginService) => $plugin->isTheme() && $pluginService->hasThemePluginEnabled())
@@ -160,13 +178,13 @@ class PluginResource extends Resource
Notification::make()
->success()
->title(trans('admin/plugin.notifications.updated'))
->title(trans('admin/plugin.notifications.enabled'))
->send();
}),
Action::make('disable')
Action::make('exclude_disable')
->label(trans('admin/plugin.disable'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon('tabler-x')
->icon(TablerIcon::X)
->color('warning')
->visible(fn (Plugin $plugin) => $plugin->canDisable())
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
@@ -179,13 +197,13 @@ class PluginResource extends Resource
->title(trans('admin/plugin.notifications.disabled'))
->send();
}),
Action::make('delete')
Action::make('exclude_delete')
->label(trans('filament-actions::delete.single.label'))
->authorize(fn (Plugin $plugin) => user()?->can('delete', $plugin))
->icon('tabler-trash')
->icon(TablerIcon::Trash)
->color('danger')
->requiresConfirmation()
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled)
->visible(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->deletePlugin($plugin);
@@ -196,32 +214,38 @@ class PluginResource extends Resource
->title(trans('admin/plugin.notifications.deleted'))
->send();
}),
Action::make('uninstall')
Action::make('exclude_uninstall')
->label(trans('admin/plugin.uninstall'))
->authorize(fn (Plugin $plugin) => user()?->can('update', $plugin))
->icon('tabler-terminal')
->icon(TablerIcon::Terminal)
->color('danger')
->requiresConfirmation()
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled)
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
$pluginService->uninstallPlugin($plugin);
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
->action(function (Plugin $plugin) {
try {
UninstallPlugin::dispatch(user(), $plugin);
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
Notification::make()
->success()
->title(trans('admin/plugin.notifications.uninstalled'))
->send();
Notification::make()
->success()
->title(trans('admin/plugin.notifications.uninstall_started'))
->body(trans('admin/plugin.notifications.background_info'))
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title(trans('admin/plugin.notifications.uninstall_error'))
->body($exception->getMessage())
->send();
}
}),
]),
])
->headerActions([
Action::make('import_from_file')
->label(trans('admin/plugin.import_from_file'))
->hiddenLabel()
->tooltip(trans('admin/plugin.import_from_file'))
->authorize(fn () => user()?->can('create', Plugin::class))
->icon('tabler-file-download')
->iconButton()
->iconSize(IconSize::ExtraLarge)
->icon(TablerIcon::FileDownload)
->schema([
// TODO: switch to new file upload
FileUpload::make('file')
@@ -261,11 +285,10 @@ class PluginResource extends Resource
}
}),
Action::make('import_from_url')
->label(trans('admin/plugin.import_from_url'))
->hiddenLabel()
->tooltip(trans('admin/plugin.import_from_url'))
->authorize(fn () => user()?->can('create', Plugin::class))
->icon('tabler-world-download')
->iconButton()
->iconSize(IconSize::ExtraLarge)
->icon(TablerIcon::WorldDownload)
->schema([
TextInput::make('url')
->required()
@@ -299,7 +322,7 @@ class PluginResource extends Resource
}
}),
])
->emptyStateIcon('tabler-packages')
->emptyStateIcon(TablerIcon::Packages)
->emptyStateDescription('')
->emptyStateHeading(trans('admin/plugin.no_plugins'));
}

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Roles\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Roles\RoleResource;
use App\Models\Role;
use App\Traits\Filament\CanCustomizeHeaderActions;
@@ -9,7 +10,6 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Spatie\Permission\Models\Permission;
@@ -32,9 +32,12 @@ class CreateRole extends CreateRecord
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-plus'),
Action::make('create')
->hiddenLabel()
->action('create')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/create-record.form.actions.create.label'))
->icon(TablerIcon::FilePlus),
];
}

View File

@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Roles\Pages;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Roles\RoleResource;
use App\Models\Role;
use App\Traits\Filament\CanCustomizeHeaderActions;
@@ -10,7 +11,6 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Spatie\Permission\Models\Permission;
@@ -58,12 +58,14 @@ class EditRole extends EditRecord
{
return [
DeleteAction::make()
->label(fn (Role $role) => $role->isRootAdmin() ? trans('admin/role.root_admin_delete') : ($role->users_count >= 1 ? trans('admin/role.in_use') : trans('filament-actions::delete.single.label')))
->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1)
->iconButton()->iconSize(IconSize::ExtraLarge),
$this->getSaveFormAction()->formId('form')
->iconButton()->iconSize(IconSize::ExtraLarge)
->icon('tabler-device-floppy'),
->tooltip(fn (Role $role) => $role->isRootAdmin() ? trans('admin/role.root_admin_delete') : ($role->users_count >= 1 ? trans('admin/role.in_use') : trans('filament-actions::delete.single.label')))
->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
];
}

View File

@@ -5,11 +5,7 @@ namespace App\Filament\Admin\Resources\Roles\Pages;
use App\Filament\Admin\Resources\Roles\RoleResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListRoles extends ListRecords
{
@@ -17,14 +13,4 @@ class ListRoles extends ListRecords
use CanCustomizeHeaderWidgets;
protected static string $resource = RoleResource::class;
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()
->icon('tabler-file-plus')
->iconButton()->iconSize(IconSize::ExtraLarge),
];
}
}

View File

@@ -9,7 +9,6 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewRole extends ViewRecord
{
@@ -22,8 +21,7 @@ class ViewRole extends ViewRecord
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make()
->iconButton()->iconSize(IconSize::ExtraLarge),
EditAction::make(),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Roles;
use App\Enums\CustomizationKey;
use App\Enums\TablerIcon;
use App\Filament\Admin\Resources\Roles\Pages\CreateRole;
use App\Filament\Admin\Resources\Roles\Pages\EditRole;
use App\Filament\Admin\Resources\Roles\Pages\ListRoles;
@@ -15,6 +16,8 @@ use App\Traits\Filament\CanModifyTable;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
@@ -43,7 +46,7 @@ class RoleResource extends Resource
protected static ?string $model = Role::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-users-group';
protected static string|BackedEnum|null $navigationIcon = TablerIcon::UsersGroup;
protected static ?string $recordTitleAttribute = 'name';
@@ -100,10 +103,13 @@ class RoleResource extends Resource
->hidden(fn ($record) => static::getEditAuthorizationResponse($record)->allowed()),
EditAction::make(),
])
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0)
->groupedBulkActions([
DeleteBulkAction::make(),
]);
->toolbarActions([
CreateAction::make(),
BulkActionGroup::make([
DeleteBulkAction::make('exclude_bulk_delete'),
]),
])
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0);
}
/**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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