Compare commits

...

154 Commits

Author SHA1 Message Date
Boy132
e0f38a8c99 add interface function to auto fill plugin settings form 2026-07-14 10:25:00 +02:00
sangege
1f2ff09a8b fix #2435 handle missing event names in webhook dispatcher (#2440) 2026-07-11 16:36:25 -04:00
Lance Pioch
f44ad34dc3 Fix PHPStan return.type errors (#2445) 2026-07-11 15:58:44 -04:00
Charles
4cd8b5767a Charles/fix actions (#2444)
Co-authored-by: Lance Pioch <git@lance.sh>
2026-07-11 15:31:38 -04:00
Lance Pioch
4ff5adc2a6 Monthly Shift: July 2026 (#2433)
Co-authored-by: Shift <shift@laravelshift.com>
2026-07-09 09:55:11 -04:00
Boy132
b8bbf29a7f Check email_verified when handling missing user in oauth controller (#2434) 2026-07-08 13:13:45 +02:00
Boy132
c5181c9c84 Backup hosts (#2125)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-08 07:06:59 -04:00
Boy132
54e583e0e4 Run composer update and remove package-lock.json (#2432) 2026-07-08 09:14:02 +02:00
kazamin
614153a1ed fix(plugin): avoid stale Sushi snapshot causing ModelNotFoundException on install (#2425)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Boy132 <mail@boy132.de>
2026-07-06 05:29:46 -04:00
execsuroot
7f14a72002 Fix activity log event filter query and add missing timestamp index (#2429) 2026-07-02 20:15:20 -04:00
Lance Pioch
6614ae5ea1 Laravel 13.18.1 Shift (#2428)
Co-authored-by: Shift <shift@laravelshift.com>
2026-07-02 20:12:06 -04:00
github-actions[bot]
9e0f7cee35 New Crowdin Translations (#2421)
Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com>
2026-07-02 08:24:53 -04:00
jsexp
f9793c36fd Fix missing index on activity_log_id in activity_log_subjects table (#2418)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-02 08:24:19 -04:00
wuzuantw
d4e974b806 fix(admin): fix cpu pinning showing as disabled when thread 0 is configured (#2419) 2026-07-02 08:24:02 -04:00
Boy132
00a56398fb Add more byte suffixes and make sure we don't overflow (#2417) 2026-06-25 16:41:51 +02:00
JoanFo
ab9535eb37 Fix discord webhooks (#2412) 2026-06-23 15:40:14 -04:00
JoanFo
a9cb16e042 Passkeys (#2192)
Co-authored-by: Lance Pioch <git@lance.sh>
2026-06-22 23:59:07 -04:00
github-actions[bot]
483145aac8 New Crowdin Translations (#2407)
Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com>
2026-06-22 09:10:23 -04:00
kazamin
5953a268c4 fix(i18n): add missing labels and translation keys across multiple resources (#2401) 2026-06-22 04:58:39 -04:00
Lance Pioch
2486446e23 Fix root-owned files breaking the translations PR step (#2405) 2026-06-22 00:09:20 -04:00
Lance Pioch
553c87597f Bump create-pull-request to v8 in the translations workflow (#2404) 2026-06-21 23:37:39 -04:00
Lance Pioch
0f1aa46aeb Fix translations workflow not opening PRs (#2403) 2026-06-21 23:23:03 -04:00
Charles
2771ccc421 Add proper checks for subuser permissions (#2402) 2026-06-21 16:37:42 -04:00
wuzuantw
fab5da496f fix: Egg edit form not refreshing after import or update (#2358)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2026-06-19 15:10:29 -04:00
JoanFo
ed7d4f5a6b Webhook fix + server area webhooks (#2377) 2026-06-18 23:28:14 -04:00
Harry Youd
d5df688638 add p:egg:normalize command (#2379) 2026-06-18 12:22:13 -04:00
Charles
0e75027c12 Add Jwt Scopes from upstream (#2397) 2026-06-17 06:42:03 -04:00
Boy132
475a454806 Add correct form requests for updating mount relations (eggs, nodes, servers) (#2394) 2026-06-17 12:40:33 +02:00
Charles
3a015e8081 composer update (#2398) 2026-06-17 06:34:42 -04:00
Dr.Reeves
29d774eae4 Enhance Docker publish workflow with scheduling (#2301)
Co-authored-by: Lance Pioch <git@lance.sh>
2026-06-16 12:31:50 -04:00
Boy132
2e7e4bb6d4 Add can check to suspend actions (#2393) 2026-06-16 11:29:44 +02:00
sangege
420ae022e9 Fix Crowdin language mapping and integrate automated Pint formatting (#2383) 2026-06-14 19:07:07 -04:00
Harry Youd
666a409b66 docker: parse TRUSTED_PROXIES from .env (#2378) 2026-06-12 10:31:57 -04:00
Lance Pioch
2b9c08dc5d Speed up ci checks (#2387) 2026-06-12 10:29:48 -04:00
Quinten
21ed58839c Bump all github action runners (#2386) 2026-06-11 17:21:41 +02:00
Michael (Parker) Parker
69e88a2ea7 Fix docker issues (#2375) 2026-06-11 10:00:01 -04:00
stdpi
cc2aa38ad4 Skip daemon RPC calls for servers on maintenance-mode nodes (#2371) 2026-06-11 07:05:47 -04:00
Lance Pioch
1bd4492cdc Laravel 13.15.0 Shift (#2382)
Co-authored-by: Shift <shift@laravelshift.com>
2026-06-09 19:22:44 -04:00
stdpi
90fbc87a8a Fix sed command syntax in docker-publish.yml (#2372) 2026-06-09 14:22:16 -05:00
Boy132
ac3db5af71 Add back bg-white to small stat (#2381) 2026-06-09 15:59:39 +02:00
Noah Stefano
a46443e5e4 restore daemon error surface for wings 4xx (#2325) 2026-06-09 05:56:38 -04:00
O2theC
f791a3a686 new api settings ui stuff (#2350) 2026-06-09 05:56:24 -04:00
Quinten
b3fe96f262 docker: Update php to 8.5 and node to 22 (#2380) 2026-06-08 16:02:30 -04:00
Lance Pioch
4d114571e0 Laravel 13.14.0 Shift (#2374)
Co-authored-by: Shift <shift@laravelshift.com>
2026-06-08 13:59:59 -04:00
stdpi
136224224c Fix dark mode (#2373) 2026-06-03 12:19:57 -04:00
Boy132
28452e2820 Return empty array instead of null when plugin update checking fails (#2364) 2026-06-02 21:05:11 +02:00
Boy132
be6abcefe7 Add acceptedFileTypes to font upload and add can check to save settings method (#2366) 2026-06-02 21:04:38 +02:00
Charles
2a328a24ac Improve Global Searching in admin panel (#2327) 2026-05-28 05:50:28 -04:00
Boy132
02e4527b84 Replace hardcoded pelican with config value (#2362) 2026-05-28 09:18:20 +02:00
Boy132
f676b6c794 Add error handling when trying to get extension from url (#2361) 2026-05-28 08:59:47 +02:00
Charles
cb21f13059 composer upgrade (#2359) 2026-05-27 17:10:20 -04:00
Boy132
13bb47e6eb Make sure plugin name doesn't contain path when importing (#2356) 2026-05-26 13:54:30 +02:00
sangege
447a8c1ff7 ci: Fix Crowdin synchronization paths and optimize configs (#2354) 2026-05-26 06:03:55 -04:00
sangege
a99f18b189 chore(i18n): Automate Crowdin source sync and translation pull workflows (#2339)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-05-22 05:34:15 -04:00
Lance Pioch
8f01095f9d Update readme.md (#2351) 2026-05-21 09:15:24 -04:00
Lance Pioch
110fdd2bdb Laravel 13.11.2 Shift (#2348)
Co-authored-by: Shift <shift@laravelshift.com>
2026-05-20 16:19:39 -04:00
Nikolaos Karaolidis
81bb09f4cf Add option to disable password login (#2318)
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2026-05-20 06:33:18 -04:00
Charles
dcba4a91d0 Listen for install completed event (#2345) 2026-05-20 03:36:52 -04:00
O2theC
8f7dbe5e16 move some hardcoded ratelimits for the api to env vars (#2332) 2026-05-19 07:25:18 -04:00
Noah Stefano
92bfccb2a4 Fix multi file upload (#2331) 2026-05-19 05:56:55 -04:00
Charles
11a1581a1d Update Node.js package dependencies (#2329) 2026-05-11 04:14:44 -04:00
Boy132
38620a95f4 Update to Laravel 13 & Filament v5 (+ drop php 8.2 / node.js 20 support) (#2315) 2026-05-06 08:55:54 +02:00
Nikolaos Karaolidis
d520224bc4 Add missing database driver in CLI (#2317)
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2026-05-05 12:12:58 -04:00
Nikolaos Karaolidis
4464ccb469 Add toggles for account created, subuser added/removed email notifications (#2309)
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2026-05-05 07:31:06 -04:00
Boy132
4ad305c709 Replace hardcoded hex strings (#2314) 2026-05-05 13:25:33 +02:00
Boy132
4b22010576 Allow to reorder startup_commands and docker_images (#2316) 2026-05-05 13:25:25 +02:00
Boy132
eb4e947ddb Add "backup completed" notifications (#2313) 2026-05-05 09:00:49 +02:00
Boy132
2bbfb0eef9 Fix "undefined key" error for TagsFilter (#2312) 2026-05-04 10:48:34 +02:00
Boy132
2a64ea8536 Cleanup node fqdn/ip validation (#2307) 2026-05-04 09:01:03 +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
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
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
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
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
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
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
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
Michael (Parker) Parker
8e8ce3b50f fix plugins in entrypoint (#2122) 2026-01-19 09:15:18 -05:00
742 changed files with 26804 additions and 9127 deletions

View File

@@ -13,15 +13,15 @@ jobs:
strategy:
fail-fast: true
matrix:
node-version: [20, 22]
node-version: [22, 24]
steps:
- name: Code Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
php-version: "8.5"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -30,7 +30,7 @@ jobs:
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts --no-dev
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"

View File

@@ -26,13 +26,13 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
php: [8.3, 8.4, 8.5]
env:
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
steps:
- name: Code Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get cache directory
id: composer-cache
@@ -40,7 +40,7 @@ jobs:
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
@@ -65,13 +65,13 @@ jobs:
run: php artisan migrate --force --seed
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit --parallel --do-not-fail-on-phpunit-warning
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration --parallel --do-not-fail-on-phpunit-warning
mysql:
name: MySQL
@@ -79,8 +79,8 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
database: ["mysql:8"]
php: [8.5]
database: ["mysql:8.4", "mysql:9.6"]
services:
database:
image: ${{ matrix.database }}
@@ -89,7 +89,7 @@ jobs:
MYSQL_DATABASE: testing
ports:
- 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
options: --tmpfs /var/lib/mysql:rw --health-cmd="mysqladmin ping" --health-interval=2s --health-timeout=5s --health-retries=30
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
@@ -97,7 +97,7 @@ jobs:
DB_USERNAME: root
steps:
- name: Code Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get cache directory
id: composer-cache
@@ -105,7 +105,7 @@ jobs:
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
@@ -129,14 +129,8 @@ jobs:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
- name: Unit tests
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 --parallel
run: vendor/bin/pest tests/Integration --parallel --do-not-fail-on-phpunit-warning
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@@ -147,8 +141,8 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
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 }}
@@ -157,7 +151,7 @@ jobs:
MYSQL_DATABASE: testing
ports:
- 3306
options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
options: --tmpfs /var/lib/mysql:rw --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=2s --health-timeout=5s --health-retries=30
env:
DB_CONNECTION: mariadb
DB_HOST: 127.0.0.1
@@ -165,7 +159,7 @@ jobs:
DB_USERNAME: root
steps:
- name: Code Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get cache directory
id: composer-cache
@@ -173,7 +167,7 @@ jobs:
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
@@ -197,14 +191,8 @@ jobs:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
- name: Unit tests
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 --parallel
run: vendor/bin/pest tests/Integration --parallel --do-not-fail-on-phpunit-warning
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@@ -215,8 +203,15 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
database: ["postgres:14"]
php: [8.5]
database: ["postgres:17", "postgres:18"]
include:
# postgres:17 stores data in /var/lib/postgresql/data, 18+ requires a
# single mount at /var/lib/postgresql and errors on a data submount
- database: "postgres:17"
tmpfs: /var/lib/postgresql/data
- database: "postgres:18"
tmpfs: /var/lib/postgresql
services:
database:
image: ${{ matrix.database }}
@@ -228,10 +223,11 @@ jobs:
ports:
- 5432:5432
options: >-
--tmpfs ${{ matrix.tmpfs }}:rw
--health-cmd pg_isready
--health-interval 10s
--health-interval 2s
--health-timeout 5s
--health-retries 5
--health-retries 30
env:
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
@@ -240,7 +236,7 @@ jobs:
DB_PASSWORD: postgres
steps:
- name: Code Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get cache directory
id: composer-cache
@@ -248,7 +244,7 @@ jobs:
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
@@ -269,11 +265,5 @@ jobs:
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests
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 --parallel
run: vendor/bin/pest tests/Integration --parallel --do-not-fail-on-phpunit-warning

View File

@@ -7,6 +7,8 @@ on:
release:
types:
- published
schedule:
- cron: '0 0 * * 1'
env:
REGISTRY: ghcr.io
@@ -31,13 +33,13 @@ jobs:
platform: linux/arm64
steps:
- name: Code checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Build the base PHP image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile.base
@@ -52,7 +54,7 @@ jobs:
run: docker save -o base-php-${{ matrix.arch }}.tar base-php:${{ matrix.arch }}
- name: Push the docker build to the artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: base-php-${{ matrix.arch }}.tar
path: base-php-${{ matrix.arch }}.tar
@@ -75,32 +77,54 @@ jobs:
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
if: "!contains(github.ref, 'main') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
steps:
# Fetch the latest release tag from GitHub API before checkout and metadata so we can check out the correct ref and tag the image
- name: Get latest release tag (scheduled)
id: release_info
if: github.event_name == 'schedule'
run: |
LATEST_TAG=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -w "\n%{http_code}" https://api.github.com/repos/${{ github.repository }}/releases/latest | tail -1 | head -1)
HTTP_CODE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -w "%{http_code}" -o /dev/null https://api.github.com/repos/${{ github.repository }}/releases/latest)
if [ "$HTTP_CODE" != "200" ]; then
echo "Failed to fetch latest release (HTTP $HTTP_CODE)"
exit 1
fi
LATEST_TAG=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
if [ "$LATEST_TAG" = "null" ] || [ -z "$LATEST_TAG" ]; then
echo "No release found"
exit 1
fi
echo "version_tag=${LATEST_TAG#v}" >> $GITHUB_OUTPUT
echo "schedule_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
- name: Code checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ steps.release_info.outputs.schedule_tag || github.ref }}
- name: Docker metadata
id: docker_meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
type=raw,value=latest,enable=${{ (github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false) || github.event_name == 'schedule' }}
type=raw,value=${{ steps.release_info.outputs.version_tag }},enable=${{ github.event_name == 'schedule' }}
type=ref,event=tag
type=ref,event=branch
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
# We Need to start it in host mode else it can't acces the local registry on port 5000
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -109,16 +133,16 @@ jobs:
- name: Get Build Information
id: build_info
run: |
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
echo "version_tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
# Download the base PHP image AMD64
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: base-php-amd64.tar
# Download the base PHP image ARM64
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: base-php-arm64.tar
@@ -133,27 +157,32 @@ jobs:
rm base-php-arm64.tar base-php-amd64.tar
- name: Update version in config/app.php (tag)
if: "github.event_name == 'release' && github.event.action == 'published'"
if: "(github.event_name == 'release' && github.event.action == 'published') || github.event_name == 'schedule'"
run: |
sed -i "s/'version' => 'canary',/'version' => '${{ steps.build_info.outputs.version_tag }}',/" config/app.php
VERSION="${{ github.event_name == 'schedule' && steps.release_info.outputs.version_tag || steps.build_info.outputs.version_tag }}"
if [ -z "$VERSION" ] || [ "$VERSION" = "refs/heads/main" ]; then
echo "Invalid version tag, refusing to update config"
exit 1
fi
sed -i "s|'version' => 'canary',|'version' => '$VERSION',|" config/app.php
- name: Build and Push (tag)
uses: docker/build-push-action@v6
if: "github.event_name == 'release' && github.event.action == 'published'"
uses: docker/build-push-action@v7
if: "(github.event_name == 'release' && github.event.action == 'published') || github.event_name == 'schedule'"
with:
context: .
file: ./Dockerfile
push: true
platforms: 'linux/amd64,linux/arm64'
build-args: |
VERSION=${{ steps.build_info.outputs.version_tag }}
VERSION=${{ github.event_name == 'schedule' && steps.release_info.outputs.version_tag || steps.build_info.outputs.version_tag }}
labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }}
cache-from: type=gha,scope=tagged${{ matrix.os }}
cache-to: type=gha,scope=tagged${{ matrix.os }},mode=max
- name: Build and Push (main)
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
if: "github.event_name == 'push' && contains(github.ref, 'main')"
with:
context: .

View File

@@ -0,0 +1,54 @@
name: Download Crowdin Translations
on:
schedule:
- cron: '0 0 * * 0' # Every Sunday at midnight
workflow_dispatch:
jobs:
download-translations:
name: Download Translations from Crowdin
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Download Translations from Crowdin
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
export_only_approved: true
push_translations: false
create_pull_request: false
crowdin_branch_name: main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Run Laravel Pint
uses: aglipanci/laravel-pint-action@8eedec46a1856977c41667f9c66d5562fa3f9c08 # 2.4
# The Crowdin and Pint steps run in Docker as root, so new translation
# files land owned by root. Reclaim them so the host-side git operations
# in create-pull-request can manage the working tree.
- name: Fix file ownership
run: sudo chown -R "$(id -u):$(id -g)" "${{ github.workspace }}"
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: update translations and format with pint'
title: 'New Crowdin Translations'
body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action), formatted by Laravel Pint.'
branch: 'i18n_crowdin_translations'
base: 'main'

View File

@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Code Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.4"
php-version: "8.5"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -35,10 +35,10 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
php: [8.3, 8.4, 8.5]
steps:
- name: Code Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get cache directory
id: composer-cache
@@ -46,7 +46,7 @@ jobs:
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}

View File

@@ -14,12 +14,12 @@ jobs:
steps:
- name: Code checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
php-version: "8.5"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@@ -28,9 +28,9 @@ jobs:
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts --no-dev
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
cache: "yarn"
- name: Install JS dependencies

32
.github/workflows/upload-sources.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Upload Crowdin Sources
on:
push:
branches: [ main ]
paths:
- 'lang/en/**'
- 'crowdin.yml'
workflow_dispatch:
jobs:
upload-sources:
name: Upload Sources to Crowdin
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Upload Sources to Crowdin
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
download_translations: false
crowdin_branch_name: main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -22,7 +22,7 @@ RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
# ================================
# Stage 1-2: Yarn Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
FROM --platform=$TARGETOS/$TARGETARCH node:22-alpine AS yarn
WORKDIR /build
@@ -68,23 +68,22 @@ RUN apk add --no-cache \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
# 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
RUN mkdir -p /pelican-data/storage /var/run/supervisord \
# Symlinks for env, database, storage
&& 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 \
&& chown -R www-data: /pelican-data .env ./storage ./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/
&& 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

View File

@@ -1,7 +1,7 @@
# ================================
# Stage 0: Build PHP Base Image
# ================================
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
FROM --platform=$TARGETOS/$TARGETARCH php:8.5-fpm-alpine
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.13-labs
# Pelican Development Dockerfile
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
FROM --platform=$TARGETOS/$TARGETARCH php:8.5-fpm-alpine AS base
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
@@ -26,7 +26,7 @@ RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
# ================================
# Stage 1-2: Yarn Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
FROM --platform=$TARGETOS/$TARGETARCH node:22-alpine AS yarn
WORKDIR /build
@@ -73,22 +73,22 @@ RUN apk add --no-cache \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
# 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
RUN mkdir -p /pelican-data/storage /var/run/supervisord \
# Symlinks for env, database, storage
&& 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 \
&& chown -R www-data: /pelican-data .env ./storage ./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/
&& 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

View File

@@ -2,8 +2,8 @@
namespace App\Checks;
use Carbon\Carbon;
use Composer\InstalledVersions;
use Illuminate\Support\Carbon;
use Spatie\Health\Checks\Checks\ScheduleCheck as BaseCheck;
use Spatie\Health\Checks\Result;

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)) {
@@ -38,14 +35,21 @@ class CheckEggUpdatesCommand extends Command
return;
}
$ext = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION));
$isYaml = in_array($ext, ['yaml', 'yml']);
$extension = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION));
throw_if(empty($extension), new Exception('Unsupported file format.'));
$isYaml = in_array($extension, ['yaml', 'yml']);
$local = $isYaml
? 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);
throw_if($remote->failed(), 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

@@ -0,0 +1,152 @@
<?php
namespace App\Console\Commands\Egg;
use App\Enums\EggFormat;
use App\Exceptions\Service\InvalidFileUploadException;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Symfony\Component\Yaml\Yaml;
class NormalizeEggCommand extends Command
{
protected $description = 'Upgrades egg json/yaml to latest format as yaml';
protected $signature = 'p:egg:normalize {file} {--delete-original}';
public function __construct(
private EggExporterService $exporter,
) {
parent::__construct();
}
public function handle(EggImporterService $importer): int
{
$inputFile = $this->argument('file');
$unparsed = file_get_contents($inputFile);
if ($unparsed === false) {
$this->error("Failed to read file: {$inputFile}");
return Command::FAILURE;
}
$extension = strtolower(pathinfo($inputFile, PATHINFO_EXTENSION));
$this->info("Importing {$inputFile}");
$format = match ($extension) {
'yaml', 'yml' => EggFormat::YAML,
'json' => EggFormat::JSON,
default => null,
};
if (is_null($format)) {
$this->error(" -> unsupported extension {$extension} for {$inputFile}");
return Command::FAILURE;
}
try {
$eggArray = $importer->parse($unparsed, $format);
} catch (InvalidFileUploadException) {
$this->error(" -> unsupported file version, is it actually an egg? ({$inputFile})");
return Command::FAILURE;
}
if (
!array_key_exists('meta', $eggArray) ||
!is_array($eggArray['meta'])
) {
$this->error(" -> does not contain existing meta or meta is not array, is it actually an egg? ({$inputFile})");
return Command::FAILURE;
}
if (!array_key_exists('exported_at', $eggArray)) {
$this->error(" -> does not contain existing exported_at, is it actually an egg? ({$inputFile})");
return Command::FAILURE;
}
if (
!array_key_exists('update_url', $eggArray['meta']) ||
!is_string($eggArray['meta']['update_url'])
) {
$this->error(" -> does not contain existing meta.update_url or is not a string, is it actually an egg? ({$inputFile})");
return Command::FAILURE;
}
// We upgraded our in-memory array when we imported, so set to latest version before export
$eggArray['meta']['version'] = Egg::EXPORT_VERSION;
$eggArray['meta']['update_url'] = self::replaceExtension($eggArray['meta']['update_url']);
self::fixVariableRules($eggArray);
$outputFile = self::replaceExtension($inputFile);
if ($outputFile === $inputFile && !$this->hasFileChanged($unparsed, $eggArray)) {
$this->info(' -> no changes required');
return Command::SUCCESS;
} else {
$eggArray['exported_at'] = Carbon::now()->toAtomString();
}
$this->info(" -> exporting to {$outputFile}");
$yaml = $this->eggToYaml($eggArray);
if (file_put_contents($outputFile, $yaml) === false) {
$this->error(" -> failed to write output file: {$outputFile}");
return Command::FAILURE;
}
if ($this->option('delete-original') && $outputFile !== $inputFile) {
$this->info(' -> deleting input file as requested');
if (!unlink($inputFile)) {
$this->warn(" -> failed to delete original file: {$inputFile}");
}
}
return Command::SUCCESS;
}
private static function replaceExtension(string $path): string
{
return preg_replace('/^(.*\.)(?:yml|json|yaml)$/', '$1yaml', $path) ?? $path;
}
/**
* @param array<string, mixed> $eggArray
*/
private static function fixVariableRules(array &$eggArray): void
{
if (!array_key_exists('variables', $eggArray)) {
return;
}
foreach ($eggArray['variables'] as &$var) {
unset($var['field_type']);
if (!array_key_exists('rules', $var)) {
continue;
}
$var['rules'] = is_array($var['rules']) ? $var['rules'] : explode('|', $var['rules']);
}
}
/**
* @param array<string, mixed> $eggArray
*/
private function hasFileChanged(string $unparsed, array $eggArray): bool
{
$yaml = $this->eggToYaml($eggArray);
return $unparsed !== $yaml;
}
/**
* @param array<string, mixed> $eggArray
*/
private function eggToYaml(array $eggArray): string
{
return Yaml::dump($this->exporter->yamlExport($eggArray), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK | Yaml::DUMP_OBJECT_AS_MAP);
}
}

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

@@ -16,6 +16,7 @@ class DatabaseSettingsCommand extends Command
'sqlite' => 'SQLite (recommended)',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
'pgsql' => 'PostgreSQL',
];
protected $description = 'Configure database settings for the Panel.';
@@ -23,10 +24,10 @@ class DatabaseSettingsCommand extends Command
protected $signature = 'p:environment:database
{--driver= : The database driver backend to use.}
{--database= : The database to use.}
{--host= : The connection address for the MySQL/ MariaDB server.}
{--port= : The connection port for the MySQL/ MariaDB server.}
{--username= : Username to use when connecting to the MySQL/ MariaDB server.}
{--password= : Password to use for the MySQL/ MariaDB database.}';
{--host= : The connection address for the MySQL/ MariaDB/ PostgreSQL server.}
{--port= : The connection port for the MySQL/ MariaDB/ PostgreSQL server.}
{--username= : Username to use when connecting to the MySQL/ MariaDB/ PostgreSQL server.}
{--password= : Password to use for the MySQL/ MariaDB/ PostgreSQL database.}';
/** @var array<array-key, mixed> */
protected array $variables = [];
@@ -176,6 +177,68 @@ class DatabaseSettingsCommand extends Command
return $this->handle();
}
return 1;
}
} elseif ($this->variables['DB_CONNECTION'] === 'pgsql') {
$this->output->note(trans('commands.database_settings.DB_HOST_note'));
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
'Database Host',
config('database.connections.pgsql.host', '127.0.0.1')
);
$this->variables['DB_PORT'] = $this->option('port') ?? $this->ask(
'Database Port',
config('database.connections.pgsql.port', 5432)
);
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
'Database Name',
config('database.connections.pgsql.database', 'panel')
);
$this->output->note(trans('commands.database_settings.DB_USERNAME_note'));
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
'Database Username',
config('database.connections.pgsql.username', 'pelican')
);
$askForPgSQLPassword = true;
if (!empty(config('database.connections.pgsql.password')) && $this->input->isInteractive()) {
$this->variables['DB_PASSWORD'] = config('database.connections.pgsql.password');
$askForPgSQLPassword = $this->confirm(trans('commands.database_settings.DB_PASSWORD_note'));
}
if ($askForPgSQLPassword) {
$this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password');
}
try {
// Test connection
config()->set('database.connections._panel_command_test', [
'driver' => 'pgsql',
'host' => $this->variables['DB_HOST'],
'port' => $this->variables['DB_PORT'],
'database' => $this->variables['DB_DATABASE'],
'username' => $this->variables['DB_USERNAME'],
'password' => $this->variables['DB_PASSWORD'],
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
]);
$this->database->connection('_panel_command_test')->getPdo();
} catch (PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the PostgreSQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(trans('commands.database_settings.DB_error_2'));
if ($this->confirm(trans('commands.database_settings.go_back'))) {
$this->database->disconnect('_panel_command_test');
return $this->handle();
}
return 1;
}
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Maintenance;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Carbon;
use SplFileInfo;
class CleanServiceBackupFilesCommand extends Command

View File

@@ -16,9 +16,7 @@ class PruneOrphanedBackupsCommand extends Command
public function handle(): void
{
$since = $this->option('prune-age') ?? config('backups.prune_age', 360);
if (!$since || !is_digit($since)) {
throw new InvalidArgumentException('The "--prune-age" argument must be a value greater than 0.');
}
throw_if(!$since || !is_digit($since), new InvalidArgumentException('The "--prune-age" argument must be a value greater than 0.'));
$query = Backup::query()
->whereNull('completed_at')

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

@@ -18,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!');

View File

@@ -18,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!');

View File

@@ -17,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!');

View File

@@ -6,6 +6,11 @@ use Filament\Schemas\Components\Component;
interface HasPluginSettings
{
/**
* @return array<string, mixed>
*/
public function getSettingsFormData(): array;
/**
* @return Component[]
*/

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,34 +24,25 @@ 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,
};
}
public function getColor(bool $hex = false): string
public function getColor(): string
{
if ($hex) {
return match ($this) {
self::Created, self::Restarting => '#2563EB',
self::Starting, self::Paused, self::Removing, self::Stopping => '#D97706',
self::Running => '#22C55E',
self::Exited, self::Missing, self::Dead, self::Offline => '#EF4444',
};
}
return match ($this) {
self::Created => 'primary',
self::Starting => 'warning',

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

@@ -0,0 +1,12 @@
<?php
namespace App\Enums;
enum NodeJwtScope: string
{
case BackupDownload = 'backup-download';
case FileDownload = 'file-download';
case FileUpload = 'file-upload';
case ServerTransfer = 'transfer';
case Websocket = 'websocket';
}

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

@@ -43,12 +43,12 @@ enum ResourceLimit: string
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),
self::Websocket => Limit::perMinutes(config('http.rate_limit.websocket_period'), config('http.rate_limit.websocket')),
self::BackupRestore => Limit::perMinutes(config('http.rate_limit.backup_restore_period'), config('http.rate_limit.backup_restore')),
self::DatabaseCreate => Limit::perMinutes(config('http.rate_limit.database_create_period'), config('http.rate_limit.database_create')),
self::SubuserCreate => Limit::perMinutes(config('http.rate_limit.subuser_create_period'), config('http.rate_limit.subuser_create')),
self::FilePull => Limit::perMinutes(config('http.rate_limit.file_pull_period'), config('http.rate_limit.file_pull')),
default => Limit::perMinutes(config('http.rate_limit.default_period'), config('http.rate_limit.default')),
};
}

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,27 +15,18 @@ 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,
};
}
public function getColor(bool $hex = false): string
public function getColor(): string
{
if ($hex) {
return match ($this) {
self::Installing, self::RestoringBackup => '#2563EB',
self::Suspended => '#D97706',
self::InstallFailed, self::ReinstallFailed => '#EF4444',
};
}
return match ($this) {
self::Installing => 'primary',
self::InstallFailed => 'danger',

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,
};
}

5997
app/Enums/TablerIcon.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Enums;
enum WebhookScope: string
{
case Global = 'global';
case Server = 'server';
public function getLabel(): string
{
return match ($this) {
self::Global => 'Global',
self::Server => 'Server',
};
}
}

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\Server;
use App\Events\Event;
use App\Models\Backup;
use Illuminate\Queue\SerializesModels;
class BackupCompleted extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Backup $backup) {}
}

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

@@ -11,6 +11,6 @@ class FileSizeTooLargeException extends DisplayException
*/
public function __construct()
{
parent::__construct('The file you are attempting to open is too large to view in the file editor.');
parent::__construct(trans('exceptions.server.file_too_large'));
}
}

View File

@@ -15,17 +15,17 @@ class ServerStateConflictException extends ConflictHttpException
*/
public function __construct(Server $server, ?Throwable $previous = null)
{
$message = 'This server is currently in an unsupported state, please try again later.';
$message = trans('exceptions.server.state_conflict');
if ($server->isSuspended()) {
$message = 'This server is currently suspended and the functionality requested is unavailable.';
$message = trans('exceptions.server.suspended');
} elseif ($server->node->isUnderMaintenance()) {
$message = 'The node of this server is currently under maintenance and the functionality requested is unavailable.';
$message = trans('exceptions.server.maintenance');
} elseif (!$server->isInstalled()) {
$message = 'This server has not yet completed its installation process, please try again later.';
$message = trans('exceptions.server.marked_as_failed');
} elseif ($server->status === ServerState::RestoringBackup) {
$message = 'This server is currently restoring from a backup, please try again later.';
$message = trans('exceptions.server.restoring_backup');
} elseif (!is_null($server->transfer)) {
$message = 'This server is currently being transferred to a new machine, please try again later.';
$message = trans('exceptions.server.transferring');
}
parent::__construct($message, $previous);

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,66 @@
<?php
namespace App\Extensions\BackupAdapter\Schemas;
use App\Enums\NodeJwtScope;
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))
->setScopes(NodeJwtScope::BackupDownload)
->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;
@@ -19,7 +21,7 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
public function isEnabled(): bool
{
return env('CAPTCHA_TURNSTILE_ENABLED', false);
return (bool) env('CAPTCHA_TURNSTILE_ENABLED', false);
}
public function getFormComponent(): Component
@@ -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;
}
/**
@@ -73,9 +75,7 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
{
$captchaResponse ??= request()->get('cf-turnstile-response');
if (!$secret = env('CAPTCHA_TURNSTILE_SECRET_KEY')) {
throw new Exception('Turnstile secret key is not defined.');
}
throw_unless($secret = env('CAPTCHA_TURNSTILE_SECRET_KEY'), new Exception('Turnstile secret key is not defined.'));
$response = Http::asJson()
->timeout(15)
@@ -99,9 +99,7 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
};
}
if (!$this->verifyDomain($response['hostname'] ?? '')) {
throw new Exception('Domain verification failed.');
}
throw_unless($this->verifyDomain($response['hostname'] ?? ''), new Exception('Domain verification failed.'));
}
private function verifyDomain(string $hostname): bool

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;
@@ -51,13 +52,13 @@ class GSLTokenSchema implements FeatureSchemaInterface
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token')
->modalHeading(trans('server/feature.gsl_token.heading'))
->modalDescription(trans('server/feature.gsl_token.description'))
->modalSubmitActionLabel(trans('server/feature.gsl_token.submit'))
->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupUpdate, $server))
->schema([
TextEntry::make('info')
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
->label(new HtmlString(Blade::render(trans('server/feature.gsl_token.info')))),
TextInput::make('gsltoken')
->label('GSL Token')
->rules([
@@ -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),
@@ -103,13 +104,13 @@ class GSLTokenSchema implements FeatureSchemaInterface
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('GSL Token updated')
->body('Server will restart now.')
->title(trans('server/feature.gsl_token.updated'))
->body(trans('server/feature.restart_now'))
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not update GSL Token')
->title(trans('server/feature.gsl_token.failed'))
->body($exception->getMessage())
->danger()
->send();

View File

@@ -41,15 +41,15 @@ class JavaVersionSchema implements FeatureSchemaInterface
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Unsupported Java Version')
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
->modalSubmitActionLabel('Update Docker Image')
->modalHeading(trans('server/feature.java_version.heading'))
->modalDescription(trans('server/feature.java_version.description'))
->modalSubmitActionLabel(trans('server/feature.java_version.submit'))
->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupDockerImage, $server))
->schema([
TextEntry::make('java')
->label('Please select a supported version from the list below to continue starting the server.'),
->label(trans('server/feature.java_version.select_version')),
Select::make('image')
->label('Docker Image')
->label(trans('server/feature.java_version.docker_image'))
->disabled(fn () => !in_array($server->image, $server->egg->docker_images))
->options(fn () => collect($server->egg->docker_images)->mapWithKeys(fn ($key, $value) => [$key => $value]))
->selectablePlaceholder(false)
@@ -73,13 +73,13 @@ class JavaVersionSchema implements FeatureSchemaInterface
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('Docker image updated')
->body('Server will restart now.')
->title(trans('server/feature.java_version.updated'))
->body(trans('server/feature.restart_now'))
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not update docker image')
->title(trans('server/feature.java_version.failed'))
->body($exception->getMessage())
->danger()
->send();

View File

@@ -32,9 +32,9 @@ class MinecraftEulaSchema implements FeatureSchemaInterface
{
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Minecraft EULA')
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
->modalSubmitActionLabel('I Accept')
->modalHeading(trans('server/feature.eula.heading'))
->modalDescription(new HtmlString(Blade::render(trans('server/feature.eula.description'))))
->modalSubmitActionLabel(trans('server/feature.eula.accept'))
->action(function (DaemonFileRepository $fileRepository, DaemonServerRepository $serverRepository) {
try {
/** @var Server $server */
@@ -45,13 +45,13 @@ class MinecraftEulaSchema implements FeatureSchemaInterface
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('Minecraft EULA accepted')
->body('Server will restart now.')
->title(trans('server/feature.eula.accepted'))
->body(trans('server/feature.restart_now'))
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not accept Minecraft EULA')
->title(trans('server/feature.eula.failed'))
->body($exception->getMessage())
->danger()
->send();

View File

@@ -2,9 +2,9 @@
namespace App\Extensions\Features\Schemas;
use App\Enums\TablerIcon;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class PIDLimitSchema implements FeatureSchemaInterface
@@ -31,36 +31,10 @@ class PIDLimitSchema implements FeatureSchemaInterface
{
return Action::make($this->getId())
->requiresConfirmation()
->icon('tabler-alert-triangle')
->modalHeading(fn () => user()?->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML'
<p>
This server has reached the maximum process or memory limit.
</p>
<p class="mt-4">
Increasing <code>container_pid_limit</code> in the wings
configuration, <code>config.yml</code>, might help resolve
this issue.
</p>
<p class="mt-4">
<b>Note: Wings must be restarted for the configuration file changes to take effect</b>
</p>
HTML
:
<<<'HTML'
<p>
This server is attempting to use more resources than allocated. Please contact the administrator
and give them the error below.
</p>
<p class="mt-4">
<code>
pthread_create failed, Possibly out of memory or process/resource limits reached
</code>
</p>
HTML
)))
->modalCancelActionLabel('Close')
->icon(TablerIcon::AlertTriangle)
->modalHeading(fn () => user()?->isAdmin() ? trans('server/feature.pid_limit.heading_admin') : trans('server/feature.pid_limit.heading_user'))
->modalDescription(new HtmlString(user()?->isAdmin() ? trans('server/feature.pid_limit.description_admin') : trans('server/feature.pid_limit.description_user')))
->modalCancelActionLabel(trans('server/feature.close'))
->action(fn () => null);
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class SteamDiskSpaceSchema implements FeatureSchemaInterface
@@ -27,28 +26,9 @@ class SteamDiskSpaceSchema implements FeatureSchemaInterface
{
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Out of available disk space...')
->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process.
</p>
<p class="mt-4">
Ensure the machine has enough disk space by typing{' '}
<code class="rounded py-1 px-2">df -h</code> on the machine hosting
this server. Delete files or increase the available disk space to resolve the issue.
</p>
HTML
:
<<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process. Please get in touch with the administrator(s) and inform them of disk space issues.
</p>
HTML
)))
->modalCancelActionLabel('Close')
->modalHeading(trans('server/feature.steam_disk_space.heading'))
->modalDescription(new HtmlString(user()?->isAdmin() ? trans('server/feature.steam_disk_space.description_admin') : trans('server/feature.steam_disk_space.description_user')))
->modalCancelActionLabel(trans('server/feature.close'))
->action(fn () => null);
}
}

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

@@ -3,6 +3,7 @@
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;
@@ -29,7 +30,7 @@ interface OAuthSchemaInterface
/** @return Step[] */
public function getSetupSteps(): array;
public function getIcon(): ?string;
public function getIcon(): null|string|BackedEnum;
public function getHexColor(): ?string;

View File

@@ -73,11 +73,11 @@ final class AuthentikSchema extends OAuthSchema
public function getName(): string
{
return env('OAUTH_AUTHENTIK_DISPLAY_NAME', 'Authentik');
return (string) env('OAUTH_AUTHENTIK_DISPLAY_NAME', 'Authentik');
}
public function getHexColor(): string
{
return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d');
return (string) env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d');
}
}

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;
@@ -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,8 +2,10 @@
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;
@@ -61,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)
@@ -72,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)
@@ -106,7 +108,7 @@ abstract class OAuthSchema implements OAuthSchemaInterface
return "OAUTH_{$id}_ENABLED";
}
public function getIcon(): ?string
public function getIcon(): null|string|BackedEnum
{
return null;
}
@@ -118,22 +120,20 @@ abstract class OAuthSchema implements OAuthSchemaInterface
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false);
return (bool) env($this->getConfigKey(), false);
}
public function shouldCreateMissingUser(OAuthUser $user): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
return (bool) env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
return (bool) env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
}
}

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

@@ -44,4 +44,17 @@ class Fractal extends SpatieFractal
return parent::createData();
}
/**
* Perform the transformation to array.
*
* @return array<mixed>
*
* @throws InvalidTransformation
* @throws NoTransformerSpecified
*/
public function toArray(): array
{
return parent::toArray() ?? [];
}
}

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;
@@ -23,7 +25,7 @@ class Dashboard extends BaseDashboard
public function getHeading(): string
{
return trans('admin/dashboard.heading');
return trans('admin/dashboard.heading', ['app' => config('app.name')]);
}
public function getSubheading(): string

View File

@@ -2,11 +2,12 @@
namespace App\Filament\Admin\Pages;
use Carbon\Carbon;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Carbon;
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';
@@ -40,16 +41,16 @@ class Health extends Page
public static function canAccess(): bool
{
return user()?->can('view health');
return user()?->can('view health') ?? false;
}
protected function getActions(): array
{
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,32 @@
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\FilamentLogViewerPlugin;
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\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
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 +36,13 @@ class ListLogs extends BaseListLogs
{
return parent::table($table)
->emptyStateHeading(trans('admin/log.empty_table'))
->emptyStateIcon('tabler-check')
->emptyStateIcon(TablerIcon::Check)
->toolbarActions([
BulkActionGroup::make([
self::exclude_downloadBulkAction(),
self::exclude_deleteBulkAction(),
]),
])
->columns([
NameColumn::make('date'),
LevelColumn::make(Level::ALL)
@@ -46,24 +60,28 @@ 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(function (array $record): string {
return trans('filament-log-viewer::log.table.actions.download.label', ['log' => ParseDateUseCase::execute((string) ($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) {
->action(function (array $record): void {
$prefix = config('filament-log-viewer.pattern.prefix', 'laravel-');
$extension = config('filament-log-viewer.pattern.extension', '.log');
$logPath = storage_path('logs/' . $prefix . $record['date'] . $extension);
$logPath = storage_path('logs/' . $prefix . (string) ($record['date'] ?? '') . $extension);
if (!file_exists($logPath)) {
Notification::make()
->title(trans('admin/log.actions.log_not_found'))
->body(trans('admin/log.actions.log_not_found_description', ['filename' => $record['date']]))
->body(trans('admin/log.actions.log_not_found_description', ['filename' => (string) ($record['date'] ?? '')]))
->danger()
->send();
@@ -71,9 +89,9 @@ class ListLogs extends BaseListLogs
}
$lines = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$totalLines = count($lines);
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$totalLines = is_array($lines) ? count($lines) : 0;
$uploadLines = $totalLines <= 1000 ? ($lines ?: []) : array_slice($lines ?: [], -1000);
$content = implode("\n", (array) $uploadLines);
try {
$response = Http::timeout(10)
@@ -93,14 +111,14 @@ class ListLogs extends BaseListLogs
}
$data = $response->json();
$url = $data['url'];
$url = (string) ($data['url'] ?? '');
Notification::make()
->title(trans('admin/log.actions.log_upload'))
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
Action::make('exclude_viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
@@ -119,7 +137,53 @@ class ListLogs extends BaseListLogs
}
}),
DeleteAction::make()
->iconSize(IconSize::Medium)->iconButton(),
->icon(TablerIcon::Trash)->iconButton(),
]);
}
private static function exclude_downloadBulkAction(): BulkAction
{
return BulkAction::make('exclude_download')
->label(trans('filament-log-viewer::log.table.actions.download.bulk.label'))
->icon(TablerIcon::FileZip)
->color('success')
->requiresConfirmation()
->modalHeading(trans('filament-log-viewer::log.table.actions.download.bulk.label'))
->failureNotificationTitle(trans('filament-log-viewer::log.table.actions.download.bulk.error'))
->action(self::exclude_downloadBulkAction_getAction(...));
}
/**
* @param Collection<array-key, array<string, mixed>> $records
*/
private static function exclude_downloadBulkAction_getAction(
BulkAction $action,
Collection $records,
): ?BinaryFileResponse {
try {
$logs = $records->pluck('date')->all();
return FilamentLogViewerPlugin::make()->downloadLogs($logs);
} catch (\Exception) {
$action->failure();
return null;
}
}
private static function exclude_deleteBulkAction(): DeleteBulkAction
{
return DeleteBulkAction::make('exclude_delete')
->modalHeading(trans('filament-log-viewer::log.table.actions.delete.bulk.label'))
->action(function (DeleteBulkAction $action) {
$action->process(function (Collection $records): void {
/** @var array<string, mixed> $record */
foreach ($records as $record) {
FilamentLogViewerPlugin::make()->deleteLog((string) ($record['date'] ?? ''));
}
});
$action->success();
});
}
}

View File

@@ -2,16 +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;
@@ -28,16 +30,17 @@ use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Text;
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;
@@ -58,7 +61,7 @@ class Settings extends Page implements HasSchemas
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';
@@ -68,6 +71,8 @@ class Settings extends Page implements HasSchemas
protected CaptchaService $captchaService;
protected IconFactory $iconFactory;
/** @var array<mixed>|null */
public ?array $data = [];
@@ -76,16 +81,17 @@ 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
{
return user()?->can('view settings');
return user()?->can('view settings') ?? false;
}
public function getTitle(): string
@@ -120,29 +126,29 @@ class Settings extends Page implements HasSchemas
return [
Tab::make('general')
->label(trans('admin/setting.navigation.general'))
->icon('tabler-home')
->icon(TablerIcon::Home)
->schema($this->generalSettings()),
Tab::make('captcha')
->label(trans('admin/setting.navigation.captcha'))
->icon('tabler-shield')
->icon(TablerIcon::Shield)
->schema($this->captchaSettings())
->columns(1),
Tab::make('mail')
->label(trans('admin/setting.navigation.mail'))
->icon('tabler-mail')
->icon(TablerIcon::Mail)
->schema($this->mailSettings()),
Tab::make('backup')
->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box')
->icon(TablerIcon::Box)
->schema($this->backupSettings()),
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth')
->icon(TablerIcon::BrandOauth)
->schema($this->oauthSettings())
->columns(1),
Tab::make('misc')
->label(trans('admin/setting.navigation.misc'))
->icon('tabler-tool')
->icon(TablerIcon::Tool)
->schema($this->miscSettings()),
];
}
@@ -162,12 +168,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'),
@@ -178,8 +184,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))
@@ -196,8 +202,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))
@@ -244,16 +250,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();
@@ -295,7 +301,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([
@@ -349,9 +355,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) {
@@ -485,16 +491,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()
@@ -514,41 +510,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'))),
]),
];
}
@@ -563,26 +524,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'))
@@ -627,8 +596,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()
@@ -638,8 +607,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()
@@ -670,10 +639,40 @@ class Settings extends Page implements HasSchemas
->collapsible()
->collapsed()
->schema([
Toggle::make('PANEL_SEND_ACCOUNT_CREATED_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.account_created'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_ACCOUNT_CREATED_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_ACCOUNT_CREATED_NOTIFICATION', config('panel.email.send_account_created_notification'))),
Toggle::make('PANEL_SEND_ADDED_TO_SERVER_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.added_to_server'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_ADDED_TO_SERVER_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_ADDED_TO_SERVER_NOTIFICATION', config('panel.email.send_added_to_server_notification'))),
Toggle::make('PANEL_SEND_REMOVED_FROM_SERVER_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.removed_from_server'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REMOVED_FROM_SERVER_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_REMOVED_FROM_SERVER_NOTIFICATION', config('panel.email.send_removed_from_server_notification'))),
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()
@@ -682,14 +681,24 @@ 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()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
Toggle::make('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION')
->label(trans('admin/setting.misc.mail_notifications.backup_completed'))
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION', config('panel.email.send_backup_completed_notification'))),
]),
Section::make(trans('admin/setting.misc.connections.title'))
->description(trans('admin/setting.misc.connections.helper'))
@@ -731,8 +740,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()
@@ -741,24 +750,172 @@ class Settings extends Page implements HasSchemas
]),
Section::make(trans('admin/setting.misc.api.title'))
->description(trans('admin/setting.misc.api.helper'))
->columns()
->columns(1)
->collapsible()
->collapsed()
->schema([
TextInput::make('APP_API_CLIENT_RATELIMIT')
->label(trans('admin/setting.misc.api.client_rate'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.rpm'))
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
TextInput::make('APP_API_APPLICATION_RATELIMIT')
->label(trans('admin/setting.misc.api.app_rate'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.rpm'))
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
Fieldset::make()
->label(trans('admin/setting.misc.api.client_title'))
->schema([
TextInput::make('APP_API_CLIENT_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
TextInput::make('APP_API_CLIENT_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_CLIENT_RATELIMIT_PERIOD', config('http.rate_limit.client_period'))),
Text::make(trans('admin/setting.misc.api.client_helper')),
]),
Fieldset::make()
->label(trans('admin/setting.misc.api.application_title'))
->schema([
TextInput::make('APP_API_APPLICATION_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
TextInput::make('APP_API_APPLICATION_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_APPLICATION_RATELIMIT_PERIOD', config('http.rate_limit.application_period'))),
Text::make(trans('admin/setting.misc.api.application_helper')),
]),
Fieldset::make()
->label(trans('admin/setting.misc.api.password_reset_title'))
->schema([
TextInput::make('APP_API_PASSWORD_RESET_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_PASSWORD_RESET_RATELIMIT', config('http.rate_limit.password_reset'))),
TextInput::make('APP_API_PASSWORD_RESET_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_PASSWORD_RESET_RATELIMIT_PERIOD', config('http.rate_limit.password_reset_period'))),
Text::make(trans('admin/setting.misc.api.password_reset_helper')),
]),
Fieldset::make()
->label(trans('admin/setting.misc.api.websocket_title'))
->schema([
TextInput::make('APP_API_WEBSOCKET_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_WEBSOCKET_RATELIMIT', config('http.rate_limit.websocket'))),
TextInput::make('APP_API_WEBSOCKET_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_WEBSOCKET_RATELIMIT_PERIOD', config('http.rate_limit.websocket_period'))),
Text::make(trans('admin/setting.misc.api.websocket_helper')),
]),
Fieldset::make()
->label(trans('admin/setting.misc.api.backup_restore_title'))
->schema([
TextInput::make('APP_API_BACKUP_RESTORE_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_BACKUP_RESTORE_RATELIMIT', config('http.rate_limit.backup_restore'))),
TextInput::make('APP_API_BACKUP_RESTORE_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_BACKUP_RESTORE_RATELIMIT_PERIOD', config('http.rate_limit.backup_restore_period'))),
Text::make(trans('admin/setting.misc.api.backup_restore_helper')),
]),
Fieldset::make()
->label(trans('admin/setting.misc.api.database_create_title'))
->schema([
TextInput::make('APP_API_DATABASE_CREATE_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_DATABASE_CREATE_RATELIMIT', config('http.rate_limit.database_create'))),
TextInput::make('APP_API_DATABASE_CREATE_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_DATABASE_CREATE_RATELIMIT_PERIOD', config('http.rate_limit.database_create_period'))),
Text::make(trans('admin/setting.misc.api.database_create_helper')),
]),
Fieldset::make()
->label(trans('admin/setting.misc.api.subuser_create_title'))
->schema([
TextInput::make('APP_API_SUBUSER_CREATE_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_SUBUSER_CREATE_RATELIMIT', config('http.rate_limit.subuser_create'))),
TextInput::make('APP_API_SUBUSER_CREATE_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_SUBUSER_CREATE_RATELIMIT_PERIOD', config('http.rate_limit.subuser_create_period'))),
Text::make(trans('admin/setting.misc.api.subuser_create_helper')),
]),
Fieldset::make()
->label(trans('admin/setting.misc.api.file_pull_title'))
->schema([
TextInput::make('APP_API_FILE_PULL_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_FILE_PULL_RATELIMIT', config('http.rate_limit.file_pull'))),
TextInput::make('APP_API_FILE_PULL_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_FILE_PULL_RATELIMIT_PERIOD', config('http.rate_limit.file_pull_period'))),
Text::make(trans('admin/setting.misc.api.file_pull_helper')),
]),
Fieldset::make()
->label(trans('admin/setting.misc.api.default_title'))
->schema([
TextInput::make('APP_API_DEFAULT_RATELIMIT')
->label(trans('admin/setting.misc.api.requests_per_period'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_DEFAULT_RATELIMIT', config('http.rate_limit.default'))),
TextInput::make('APP_API_DEFAULT_RATELIMIT_PERIOD')
->label(trans('admin/setting.misc.api.period_length'))
->required()
->numeric()
->minValue(1)
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_DEFAULT_RATELIMIT_PERIOD', config('http.rate_limit.default_period'))),
Text::make(trans('admin/setting.misc.api.default_helper')),
]),
]),
Section::make(trans('admin/setting.misc.server.title'))
->description(trans('admin/setting.misc.server.helper'))
@@ -768,8 +925,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()
@@ -777,13 +934,14 @@ class Settings extends Page implements HasSchemas
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
FileUpload::make('ConsoleFonts')
FileUpload::make('console_font')
->hint(trans('admin/setting.misc.server.console_font_hint'))
->label(trans('admin/setting.misc.server.console_font_upload'))
->directory('fonts')
->disk('public')
->columnSpan(1)
->maxFiles(1)
->acceptedFileTypes(['font/*'])
->preserveFilenames(),
]),
Section::make(trans('admin/setting.misc.webhook.title'))
@@ -811,9 +969,11 @@ class Settings extends Page implements HasSchemas
public function save(): void
{
abort_unless(user()?->can('update settings'), 403);
try {
$data = $this->form->getState();
unset($data['ConsoleFonts']);
unset($data['console_font']);
$data = array_map(function ($value) {
// Convert bools to a string, so they are correctly written to the .env file
@@ -853,12 +1013,12 @@ 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,14 +20,15 @@ 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'))
@@ -77,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';
@@ -85,6 +89,7 @@ class DatabaseHostResource extends Resource
->counts('databases')
->label(trans('admin/databasehost.databases')),
TextColumn::make('nodes.name')
->label(trans('admin/databasehost.nodes'))
->badge()
->placeholder(trans('admin/databasehost.no_nodes')),
])
@@ -94,10 +99,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;
@@ -67,11 +66,11 @@ class DatabasesRelationManager extends RelationManager
DateTimeColumn::make('created_at')
->label(trans('admin/databasehost.table.created_at')),
])
->emptyStateHeading(trans('admin/databasehost.no_databases'))
->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';
@@ -32,7 +34,11 @@ class EggResource extends Resource
public static function getNavigationGroup(): ?string
{
return user()?->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
if (user()?->getCustomization(CustomizationKey::TopNavigation)) {
return null;
}
return trans('admin/dashboard.server');
}
public static function getNavigationLabel(): string

View File

@@ -3,6 +3,7 @@
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;
@@ -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;
@@ -47,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),
];
}
@@ -100,6 +103,7 @@ class CreateEgg extends CreateRecord
->live()
->columnSpanFull()
->required()
->reorderable()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->keyPlaceholder('Default')
@@ -116,7 +120,7 @@ class CreateEgg extends CreateRecord
->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')),
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->default(1),
TagsInput::make('tags')
@@ -124,7 +128,7 @@ class CreateEgg extends CreateRecord
->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'))
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(),
KeyValue::make('docker_images')
@@ -132,6 +136,7 @@ class CreateEgg extends CreateRecord
->live()
->columnSpanFull()
->required()
->reorderable()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->keyPlaceholder('Java 21')
@@ -172,7 +177,7 @@ class CreateEgg extends CreateRecord
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->reorderable()->orderColumn()
->orderColumn()
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)
@@ -214,7 +219,7 @@ class CreateEgg extends CreateRecord
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->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([

View File

@@ -3,9 +3,12 @@
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;
@@ -13,12 +16,10 @@ use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use Exception;
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;
@@ -27,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;
@@ -39,10 +37,7 @@ 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
{
@@ -70,165 +65,20 @@ class EditEgg extends EditRecord
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon('tabler-egg')
->icon(TablerIcon::Egg)
->schema([
Grid::make(2)
->columnSpan(1)
->columnStart(1)
->schema([
Image::make('', '')
->hidden(fn ($record) => !$record->image)
->url(fn ($record) => $record->image)
->alt('')
->alignJustify()
Image::make('', 'icon')
->hidden(fn ($record) => !$record->icon)
->url(fn ($record) => $record->icon)
->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);
return;
}
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();
}),
]),
->columnSpanFull()
->alignJustify(),
UploadIcon::make(),
DeleteIcon::make()
->iconStoragePath(Egg::getIconStoragePath()),
]),
TextInput::make('name')
->label(trans('admin/egg.name'))
@@ -262,12 +112,13 @@ class EditEgg extends EditRecord
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
->hintIcon(TablerIcon::QuestionMark, trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->reorderable()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->valueLabel(trans('admin/egg.startup_command'))
@@ -280,7 +131,7 @@ class EditEgg extends EditRecord
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->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'))
@@ -295,6 +146,7 @@ class EditEgg extends EditRecord
->live()
->columnSpanFull()
->required()
->reorderable()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->valueLabel(trans('admin/egg.docker_uri'))
@@ -303,7 +155,7 @@ class EditEgg extends EditRecord
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon('tabler-server-cog')
->icon(TablerIcon::ServerCog)
->schema([
CopyFrom::make('copy_process_from')
->process(),
@@ -316,6 +168,7 @@ class EditEgg extends EditRecord
->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'))
@@ -324,15 +177,14 @@ class EditEgg extends EditRecord
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon('tabler-variable')
->icon(TablerIcon::Variable)
->schema([
Repeater::make('variables')
->hiddenLabel()
->grid()
->relationship('variables')
->reorderable()
->collapsible()->collapsed()
->orderColumn()
->collapsible()->collapsed()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
@@ -372,7 +224,7 @@ class EditEgg extends EditRecord
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->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([
@@ -416,7 +268,7 @@ class EditEgg extends EditRecord
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon('tabler-file-download')
->icon(TablerIcon::FileDownload)
->schema([
CopyFrom::make('copy_script_from')
->script(),
@@ -448,61 +300,28 @@ 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'))
->successNotification(fn (Egg $egg) => Notification::make()
->success()
->title(trans('admin/egg.delete_success'))
->body(trans('admin/egg.deleted', ['egg' => $egg->name]))
)
->failureNotification(fn (Egg $egg) => Notification::make()
->danger()
->title(trans('admin/egg.delete_failed'))
->body(trans('admin/egg.could_not_delete', ['egg' => $egg->name]))
)
->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'),
->multiple(false)
->after(function () {
$this->record->refresh();
$this->refreshForm();
}),
Action::make('save')
->hiddenLabel()
->action('save')
->keyBindings(['mod+s'])
->tooltip(trans('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->icon(TablerIcon::DeviceFloppy),
];
}
public function refreshForm(): void
{
$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);
$this->dispatch('setContent', content: $this->record->script_install ?? '');
}
protected function getFormActions(): array

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,15 +13,13 @@ 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;
@@ -39,6 +38,10 @@ class ListEggs extends ListRecords
*/
public function table(Table $table): Table
{
$defaultEggIcon = config('app.logo');
$defaultEggIcon = empty($defaultEggIcon) || !is_file(public_path($defaultEggIcon)) ? 'pelican.svg' : $defaultEggIcon;
$defaultEggIcon = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path($defaultEggIcon)));
return $table
->searchable(true)
->defaultPaginationPageOption(25)
@@ -46,13 +49,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)
@@ -65,19 +66,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) {
@@ -88,49 +83,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(function (Collection &$records) {
$eggsWithServers = $records->filter(fn (Egg $egg) => $egg->servers_count > 0);
->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();
}
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);
$records = $records->filter(fn (Egg $egg) => $egg->servers_count <= 0);
if ($records->isEmpty()) {
$this->halt();
}
}),
UpdateEggBulkAction::make()
->before(function (Collection &$records) {
$eggsWithoutUpdateUrl = $records->filter(fn (Egg $egg) => $egg->update_url === null);
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(', ');
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();
}
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);
$records = $records->filter(fn (Egg $egg) => $egg->update_url !== null);
if ($records->isEmpty()) {
$this->halt();
}
}),
if ($records->isEmpty()) {
$this->halt();
}
}),
]),
])
->emptyStateIcon('tabler-eggs')
->emptyStateIcon(TablerIcon::Eggs)
->emptyStateDescription('')
->emptyStateHeading(trans('admin/egg.no_eggs'))
->filters([
@@ -138,18 +138,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;
@@ -37,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';
@@ -88,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'));
}
@@ -117,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'))
@@ -127,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',
@@ -136,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()

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);
}

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