mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
606 Commits
3.x
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65a697d8f7 | ||
|
|
9515a82a75 | ||
|
|
44f5ea567f | ||
|
|
88f910f3e7 | ||
|
|
020f028008 | ||
|
|
0cb7f737b0 | ||
|
|
53aa52f519 | ||
|
|
e884eda5a7 | ||
|
|
58d1fd3917 | ||
|
|
0b0952650e | ||
|
|
aa55a7ed83 | ||
|
|
c7fa7a1bad | ||
|
|
4a3bdd78ef | ||
|
|
a1067fd4aa | ||
|
|
110cc1248b | ||
|
|
04a1ccc97e | ||
|
|
5e7f5c2a4c | ||
|
|
b804878d7b | ||
|
|
118977c8c5 | ||
|
|
c31b7b8c6a | ||
|
|
eefe59b153 | ||
|
|
cd4b7cbf9e | ||
|
|
67cb3d4816 | ||
|
|
7762e68a6c | ||
|
|
7a327ea378 | ||
|
|
b3ca7b7ac9 | ||
|
|
abc99cd928 | ||
|
|
cb638369cf | ||
|
|
9174de2d8c | ||
|
|
7cda358b66 | ||
|
|
33f6551b21 | ||
|
|
b1928e89b4 | ||
|
|
c956cd0106 | ||
|
|
5081cc3f63 | ||
|
|
8eb2c23420 | ||
|
|
cfe385f53a | ||
|
|
264d3498a6 | ||
|
|
065f3f2468 | ||
|
|
957638d4ac | ||
|
|
7d0ce1627b | ||
|
|
8cec7368ab | ||
|
|
5519931ee5 | ||
|
|
97ac0fe54b | ||
|
|
7657364208 | ||
|
|
ef1a208b95 | ||
|
|
aa82c6dd04 | ||
|
|
8ecabef6b5 | ||
|
|
a6d07ede5a | ||
|
|
f6325c07c4 | ||
|
|
7674ee0e2b | ||
|
|
5760e72b8f | ||
|
|
b6e46f758d | ||
|
|
e980877bbc | ||
|
|
dd223b47c0 | ||
|
|
639fa3399d | ||
|
|
82fd547484 | ||
|
|
d461242f08 | ||
|
|
dec1cf8e74 | ||
|
|
15caac51fb | ||
|
|
183c274a0d | ||
|
|
a8b2fb440f | ||
|
|
f8e4514998 | ||
|
|
deeebf73d3 | ||
|
|
422fc102c9 | ||
|
|
e715e92f9d | ||
|
|
73babfa2b3 | ||
|
|
e0a92d733b | ||
|
|
1e67cd9944 | ||
|
|
3946116dff | ||
|
|
b77fd3d653 | ||
|
|
f4672c6cb1 | ||
|
|
5b9e4b1729 | ||
|
|
48f715ae69 | ||
|
|
51460782cc | ||
|
|
b007e63937 | ||
|
|
4dd833562b | ||
|
|
b579f14f3f | ||
|
|
eadaec1b30 | ||
|
|
a9e58bb493 | ||
|
|
5c33c7495a | ||
|
|
f9aa8cf218 | ||
|
|
da698a3666 | ||
|
|
2808a3dd35 | ||
|
|
7ea365e8de | ||
|
|
ae399f9bad | ||
|
|
53a5ff6e6d | ||
|
|
54ae4b3dc1 | ||
|
|
859a721e17 | ||
|
|
03cbdd5bdd | ||
|
|
4c43fd1683 | ||
|
|
0c61a63191 | ||
|
|
b1f99ca8a3 | ||
|
|
0a5810358a | ||
|
|
1bae239971 | ||
|
|
597f74f105 | ||
|
|
5344d99a40 | ||
|
|
1db1a1a3e0 | ||
|
|
712b6a285b | ||
|
|
38b92ae21d | ||
|
|
c18d291f8f | ||
|
|
45c0cfe4d8 | ||
|
|
89d555f308 | ||
|
|
9b2e00ead2 | ||
|
|
d0997dca1a | ||
|
|
cca723d21e | ||
|
|
68dbd6e329 | ||
|
|
544364f061 | ||
|
|
649f319776 | ||
|
|
6ffe800e0d | ||
|
|
12af8fe51e | ||
|
|
9f5d27896a | ||
|
|
15969da41e | ||
|
|
628a6e54e3 | ||
|
|
edcd406b82 | ||
|
|
1742061807 | ||
|
|
289eda64ad | ||
|
|
cc46c463ac | ||
|
|
e6dded61a4 | ||
|
|
921b76f1e1 | ||
|
|
91a3bb969e | ||
|
|
8b62df6c53 | ||
|
|
3c14e1ffa4 | ||
|
|
095fff7ad6 | ||
|
|
0cd03e95f4 | ||
|
|
00dda7dbe4 | ||
|
|
eee1260fcc | ||
|
|
71c91e086f | ||
|
|
94bbf46659 | ||
|
|
558b8b2ea7 | ||
|
|
4bb16887c4 | ||
|
|
0c72833af7 | ||
|
|
ee3f8cd3ec | ||
|
|
b513366f35 | ||
|
|
b1d056a301 | ||
|
|
c72d3f1338 | ||
|
|
7e6231e6de | ||
|
|
4ad837ff14 | ||
|
|
d7ebde5f5f | ||
|
|
6bdd1b3ccb | ||
|
|
afd9f2eb0e | ||
|
|
8a617d416e | ||
|
|
7f4cc64d11 | ||
|
|
38e117e304 | ||
|
|
4d272a1234 | ||
|
|
e9dc6cd32c | ||
|
|
98ba2c1b8b | ||
|
|
2d643ec79f | ||
|
|
2b4625e9b9 | ||
|
|
9f40ed2f84 | ||
|
|
af797b3018 | ||
|
|
e5146e0dbb | ||
|
|
586d1b413c | ||
|
|
7d239de7f6 | ||
|
|
5f13c15c70 | ||
|
|
4e3f919d8e | ||
|
|
7efa04f1ef | ||
|
|
35ac1f863a | ||
|
|
68195ab0b7 | ||
|
|
f79dac2d13 | ||
|
|
4f176c47d2 | ||
|
|
f7e7864dfe | ||
|
|
2f8a15facd | ||
|
|
7bf175190b | ||
|
|
8866ca3d96 | ||
|
|
703e5480ff | ||
|
|
45a6c37594 | ||
|
|
340085cc9b | ||
|
|
ef9d1ab614 | ||
|
|
55a0bfdf7e | ||
|
|
d6f3934f80 | ||
|
|
f9e8adad30 | ||
|
|
08dc5753d4 | ||
|
|
0c851ea075 | ||
|
|
cf3ea38e65 | ||
|
|
b813de0467 | ||
|
|
399bed7576 | ||
|
|
5aa7128b9c | ||
|
|
92d167eb10 | ||
|
|
f02eb5bfba | ||
|
|
f348ac9f0a | ||
|
|
893b2dca89 | ||
|
|
1f5217a9d9 | ||
|
|
e84b47410a | ||
|
|
76cf4391ae | ||
|
|
7688006574 | ||
|
|
ed36041a7e | ||
|
|
bb52485606 | ||
|
|
615b70f9a2 | ||
|
|
3609873c4c | ||
|
|
7f6e4a18c1 | ||
|
|
c5b441e54a | ||
|
|
30452890f5 | ||
|
|
af18a3704f | ||
|
|
2c98693bd2 | ||
|
|
259599b441 | ||
|
|
2da058be49 | ||
|
|
8d33e20f6c | ||
|
|
b4e8f0586a | ||
|
|
6f8f5e2746 | ||
|
|
37cc3ae20d | ||
|
|
9fc46b9ae5 | ||
|
|
d409ff037c | ||
|
|
000363cd17 | ||
|
|
2beb12c04f | ||
|
|
373ede8548 | ||
|
|
e286100197 | ||
|
|
082163389a | ||
|
|
f9247c9318 | ||
|
|
5a00b8690d | ||
|
|
c0ca189536 | ||
|
|
84a3ceeae3 | ||
|
|
1c499d84cf | ||
|
|
871e93a38c | ||
|
|
1481338eb9 | ||
|
|
1e4fadda24 | ||
|
|
585fe8d1a1 | ||
|
|
f58c697d28 | ||
|
|
b32f8966e1 | ||
|
|
b18ebeefdc | ||
|
|
5fd7e419d9 | ||
|
|
602c1ed9a6 | ||
|
|
39bc87c2e2 | ||
|
|
44cc5a8132 | ||
|
|
0c7ae26313 | ||
|
|
102955bf6a | ||
|
|
72eb3ce467 | ||
|
|
d656f21cd9 | ||
|
|
8da5afb35e | ||
|
|
44e9da93b6 | ||
|
|
29e1bd4757 | ||
|
|
f1493c5139 | ||
|
|
30a668c84a | ||
|
|
9f4bf8777e | ||
|
|
dfe2e9d629 | ||
|
|
607e186082 | ||
|
|
e60c86a87e | ||
|
|
577479edaf | ||
|
|
4423baa1e7 | ||
|
|
d967681227 | ||
|
|
f391edda27 | ||
|
|
255030136f | ||
|
|
f79d304586 | ||
|
|
60772b1775 | ||
|
|
bf3ef435ae | ||
|
|
30411ccd13 | ||
|
|
bd8ca0abcf | ||
|
|
6621ece3a1 | ||
|
|
8f4b68617a | ||
|
|
1b3017222e | ||
|
|
a050fbd2d3 | ||
|
|
fe3bf88ea4 | ||
|
|
94f583fef0 | ||
|
|
8da0017eaf | ||
|
|
4e838201c6 | ||
|
|
111b8b3cda | ||
|
|
d0d388534b | ||
|
|
939b7354e4 | ||
|
|
7077693da2 | ||
|
|
9c047d0a45 | ||
|
|
f13dbfa766 | ||
|
|
d4dd1349da | ||
|
|
1db8c209fb | ||
|
|
852d2b7431 | ||
|
|
6468f85cb0 | ||
|
|
5c61865dfb | ||
|
|
3feb9d2304 | ||
|
|
f17ac6ffac | ||
|
|
04675a73fd | ||
|
|
e2353af0d8 | ||
|
|
8eaf64b5fd | ||
|
|
e5c5bc40d9 | ||
|
|
5286f446dc | ||
|
|
abbf2038a7 | ||
|
|
52026ca9e4 | ||
|
|
0b0c4bb434 | ||
|
|
22aa56d306 | ||
|
|
3b935a1eea | ||
|
|
146421ee52 | ||
|
|
9875942191 | ||
|
|
3d2b18140a | ||
|
|
ec0882cd14 | ||
|
|
48c97ee1cc | ||
|
|
17787fee18 | ||
|
|
f2a59002bc | ||
|
|
98419bc625 | ||
|
|
fb596fa4f9 | ||
|
|
7a4289cee1 | ||
|
|
488acce564 | ||
|
|
3f3b500a14 | ||
|
|
29803bbaf2 | ||
|
|
e07eabc579 | ||
|
|
e82a3b838c | ||
|
|
9761c3762d | ||
|
|
1ecfcc611f | ||
|
|
ad60b437ce | ||
|
|
cc5208cc6b | ||
|
|
a9212d9e7d | ||
|
|
efcf22d837 | ||
|
|
d4a02336aa | ||
|
|
386eba28e6 | ||
|
|
651b887a0e | ||
|
|
2982757649 | ||
|
|
04a8999e0b | ||
|
|
41ee9e563c | ||
|
|
16a16dc390 | ||
|
|
80155a17e5 | ||
|
|
26da0c5e74 | ||
|
|
27059e7b99 | ||
|
|
fdc51e03ac | ||
|
|
549ab12048 | ||
|
|
fb95a3d923 | ||
|
|
70da7c0f51 | ||
|
|
d7051bb7ed | ||
|
|
dd8f28b864 | ||
|
|
8b060e8834 | ||
|
|
7b66e1ce33 | ||
|
|
5d2248ab1f | ||
|
|
7df5f12c75 | ||
|
|
b0dadc60f2 | ||
|
|
36b7998714 | ||
|
|
d42fc88535 | ||
|
|
4ebc67aab0 | ||
|
|
5bef99611b | ||
|
|
191f1456ee | ||
|
|
a839078c7d | ||
|
|
2d7804311d | ||
|
|
6b0c1d136b | ||
|
|
383845ca62 | ||
|
|
a9b755ae2d | ||
|
|
6ed85cfdc6 | ||
|
|
137b6040ab | ||
|
|
da08b60f20 | ||
|
|
267952d750 | ||
|
|
38aea0edbe | ||
|
|
51bb60c3b1 | ||
|
|
8f2413dc7e | ||
|
|
1bdef318f0 | ||
|
|
2efb807f0b | ||
|
|
be35692125 | ||
|
|
076125485d | ||
|
|
1800f105d7 | ||
|
|
933e693897 | ||
|
|
97ff693e5c | ||
|
|
7853cdc9ed | ||
|
|
cba00d822c | ||
|
|
00502f6d4d | ||
|
|
7ffd7019a2 | ||
|
|
ea146f4715 | ||
|
|
85b250d016 | ||
|
|
7bbbba37f5 | ||
|
|
07244c38eb | ||
|
|
50f9dde280 | ||
|
|
76a3197022 | ||
|
|
f26628a546 | ||
|
|
ceb365b95c | ||
|
|
b0a2bae0b5 | ||
|
|
426b82754d | ||
|
|
2f82229048 | ||
|
|
65bfda1034 | ||
|
|
2328f07473 | ||
|
|
ad2e48cfc1 | ||
|
|
cca5e4a4c0 | ||
|
|
17ec5c7acf | ||
|
|
4708105104 | ||
|
|
19d2066a1a | ||
|
|
ae3a355a99 | ||
|
|
1996ffe724 | ||
|
|
edf9bc6f4d | ||
|
|
c31eafaf4f | ||
|
|
1a884c0cdf | ||
|
|
e343de00c0 | ||
|
|
0f360fcdd1 | ||
|
|
a9a18464dd | ||
|
|
e47beb59d2 | ||
|
|
d4ff502e08 | ||
|
|
8c9c2c080a | ||
|
|
aaf7429298 | ||
|
|
329268697b | ||
|
|
0b950832c2 | ||
|
|
788056d55d | ||
|
|
dfaff50ca1 | ||
|
|
145568237c | ||
|
|
9baaff53cd | ||
|
|
3ad622dd69 | ||
|
|
906a1d7f3e | ||
|
|
ba7a5d5126 | ||
|
|
693c65995d | ||
|
|
85f7bf30b9 | ||
|
|
53ad87a349 | ||
|
|
7a034c1abf | ||
|
|
eeee5779ba | ||
|
|
6ff9568760 | ||
|
|
7de4cf1417 | ||
|
|
db67c64da0 | ||
|
|
8476f89f19 | ||
|
|
01f89d7855 | ||
|
|
fa46f78fd5 | ||
|
|
3e239f9caa | ||
|
|
204734914d | ||
|
|
67edf4f472 | ||
|
|
7693106a44 | ||
|
|
d8f5e1506c | ||
|
|
330b3bb496 | ||
|
|
ac3a36e489 | ||
|
|
22c03c8075 | ||
|
|
bc972da982 | ||
|
|
d9738949c1 | ||
|
|
1c0e91a301 | ||
|
|
3db6593b0e | ||
|
|
2bb1caf308 | ||
|
|
807a6f02fd | ||
|
|
367b9bd154 | ||
|
|
d30accbc71 | ||
|
|
716d298b75 | ||
|
|
05c4610654 | ||
|
|
82c294ab63 | ||
|
|
ee142a26b0 | ||
|
|
dea310e9ab | ||
|
|
daf2cb0ebc | ||
|
|
2812129d00 | ||
|
|
89b6f70cde | ||
|
|
ad372a754e | ||
|
|
c0b1345e90 | ||
|
|
11e6430d42 | ||
|
|
1e10f250b4 | ||
|
|
4b23703f99 | ||
|
|
bab4315bb7 | ||
|
|
c4839708ce | ||
|
|
fa379be99b | ||
|
|
12eee6f6a2 | ||
|
|
50240933a0 | ||
|
|
69b70bf649 | ||
|
|
3e01e483fb | ||
|
|
ae189748f1 | ||
|
|
679c72d70e | ||
|
|
49e02a2574 | ||
|
|
ee735c9b77 | ||
|
|
e25ca5dfc1 | ||
|
|
422fc1a6b2 | ||
|
|
0949362da5 | ||
|
|
65f59c446e | ||
|
|
556ab76fc5 | ||
|
|
4c5072b5c0 | ||
|
|
25177d1685 | ||
|
|
5469dce6ca | ||
|
|
d659bf4349 | ||
|
|
c5008a43e7 | ||
|
|
256a961e1b | ||
|
|
d642987df4 | ||
|
|
7d0fc80a80 | ||
|
|
fa0bc96611 | ||
|
|
c492fa285f | ||
|
|
11494bbad6 | ||
|
|
c83cc073f0 | ||
|
|
b7c0829af9 | ||
|
|
e899acbdbe | ||
|
|
1f95430507 | ||
|
|
b70ab0e6cc | ||
|
|
8ec4dc1b6e | ||
|
|
9b9875a31b | ||
|
|
56e2cac85f | ||
|
|
65359f87d0 | ||
|
|
058371ba7d | ||
|
|
dc8d7aa3da | ||
|
|
e1c6545507 | ||
|
|
5c8097d9b7 | ||
|
|
6118ed91fa | ||
|
|
0814b82b7e | ||
|
|
288d3a2cff | ||
|
|
e2399eb4e2 | ||
|
|
bfe8fc66ce | ||
|
|
dd1b25604a | ||
|
|
b488253c76 | ||
|
|
56f96348f4 | ||
|
|
18cbaf7458 | ||
|
|
9add408b6b | ||
|
|
54eaf8ab0f | ||
|
|
32a3b8dd9b | ||
|
|
06c773c3b1 | ||
|
|
18e5c17ebe | ||
|
|
287c657e60 | ||
|
|
f69d0823f4 | ||
|
|
f26373dfd5 | ||
|
|
c54217f236 | ||
|
|
25c1b251a3 | ||
|
|
9fafe2f42c | ||
|
|
f25bd33f06 | ||
|
|
de02e8853d | ||
|
|
4c09905503 | ||
|
|
a9a39ae502 | ||
|
|
99693367d3 | ||
|
|
05e9f12dc4 | ||
|
|
0b1712f653 | ||
|
|
8575f1b036 | ||
|
|
4c2278a7f2 | ||
|
|
af4d1d1fee | ||
|
|
d52cb4c7d7 | ||
|
|
65acb3fd94 | ||
|
|
ecf54a3025 | ||
|
|
180bfc30a8 | ||
|
|
cbc255ddf8 | ||
|
|
15971aaa94 | ||
|
|
695ebd35a4 | ||
|
|
21247e91c7 | ||
|
|
d30471ae6a | ||
|
|
03292bb02d | ||
|
|
297e292e06 | ||
|
|
f32aa8609d | ||
|
|
f8550334dd | ||
|
|
da1bf320dd | ||
|
|
27f05b5f95 | ||
|
|
203289fd38 | ||
|
|
f9b93f284c | ||
|
|
24f9a8aeb1 | ||
|
|
c6eb6dc054 | ||
|
|
a16ef9743b | ||
|
|
6c8816c289 | ||
|
|
d4f325e6c5 | ||
|
|
93ec3bdfc4 | ||
|
|
a4fb9eea40 | ||
|
|
f201a5eaf6 | ||
|
|
6689e796a7 | ||
|
|
b2e5b4862d | ||
|
|
546dc5c449 | ||
|
|
f660611ed3 | ||
|
|
89a507de69 | ||
|
|
3cc29ba7b6 | ||
|
|
1660af94a5 | ||
|
|
c51687246e | ||
|
|
bc72d6103c | ||
|
|
cf44e46490 | ||
|
|
8311669e6c | ||
|
|
195557373c | ||
|
|
e7055242e1 | ||
|
|
6020b8d6a8 | ||
|
|
7c14a2edff | ||
|
|
6307919546 | ||
|
|
2dd53eee27 | ||
|
|
4e0aaedc86 | ||
|
|
970d2b0f0f | ||
|
|
b1cc4ef45c | ||
|
|
17c20d6b91 | ||
|
|
d05332662b | ||
|
|
42728fd9b9 | ||
|
|
732db9a5b4 | ||
|
|
a19c8e72b3 | ||
|
|
b0067c4e4b | ||
|
|
a133503256 | ||
|
|
3465d2fc64 | ||
|
|
05ae2b2ecf | ||
|
|
e00d9ed273 | ||
|
|
bf8f90f479 | ||
|
|
978a4ac0a2 | ||
|
|
aac522232f | ||
|
|
95570724e6 | ||
|
|
a96c53f407 | ||
|
|
f79ec37f5e | ||
|
|
5d38f2ece6 | ||
|
|
cf13bfb1e4 | ||
|
|
051b5d6bea | ||
|
|
a8532d1cd2 | ||
|
|
a7d8f3b79f | ||
|
|
e634dd81b1 | ||
|
|
c6f4ee3d57 | ||
|
|
f47b420785 | ||
|
|
1add3ca605 | ||
|
|
2b172e6d8b | ||
|
|
1728cbf28b | ||
|
|
766c6c08f4 | ||
|
|
400e4d783b | ||
|
|
a8b3e2bfa4 | ||
|
|
57649d1c08 | ||
|
|
dc794c64ce | ||
|
|
eda3959748 | ||
|
|
f5a0a0f8ba | ||
|
|
6e4f3f7191 | ||
|
|
e276a07f1b | ||
|
|
db9b3e9b67 | ||
|
|
d32d0692f8 | ||
|
|
ebdca47fbc | ||
|
|
957a335817 | ||
|
|
71a27862bd | ||
|
|
a2b03895d7 | ||
|
|
a93ebfd7bf | ||
|
|
3c43f3aa18 | ||
|
|
eb5c304f69 | ||
|
|
0925e141b4 | ||
|
|
ad8d087fd9 | ||
|
|
e50e3509bd | ||
|
|
090b2e6f1b | ||
|
|
0af0bea90d | ||
|
|
f452280cdb | ||
|
|
8c892ac05d | ||
|
|
65384250d6 | ||
|
|
3e7bff2446 | ||
|
|
b6d39c66d1 | ||
|
|
03e1733b7d | ||
|
|
039ac40cf7 | ||
|
|
2664ba0774 | ||
|
|
90efb4e827 | ||
|
|
946d597a13 | ||
|
|
0961d6314c | ||
|
|
0ef015bb0e | ||
|
|
36ca708850 | ||
|
|
1c539ff50c | ||
|
|
49d0865010 |
21
.env.example
21
.env.example
@@ -11,24 +11,12 @@ LOG_STACK=single
|
|||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=sqlite
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=panel
|
|
||||||
DB_USERNAME=panel
|
|
||||||
DB_PASSWORD=
|
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
CACHE_STORE=file
|
CACHE_STORE=file
|
||||||
QUEUE_CONNECTION=sync
|
QUEUE_CONNECTION=database
|
||||||
SESSION_DRIVER=file
|
SESSION_DRIVER=file
|
||||||
|
|
||||||
HASHIDS_SALT=
|
|
||||||
HASHIDS_LENGTH=8
|
|
||||||
|
|
||||||
MAIL_MAILER=log
|
MAIL_MAILER=log
|
||||||
MAIL_HOST=smtp.example.com
|
MAIL_HOST=smtp.example.com
|
||||||
MAIL_PORT=25
|
MAIL_PORT=25
|
||||||
@@ -42,3 +30,8 @@ MAIL_FROM_NAME="Pelican Admin"
|
|||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
SESSION_DOMAIN=null
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
# Set this to true, and set start & end ports to auto create allocations.
|
||||||
|
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
|
||||||
|
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
|
||||||
|
PANEL_CLIENT_ALLOCATIONS_RANGE_END=
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ module.exports = {
|
|||||||
'react/display-name': 0,
|
'react/display-name': 0,
|
||||||
'@typescript-eslint/no-explicit-any': 0,
|
'@typescript-eslint/no-explicit-any': 0,
|
||||||
'@typescript-eslint/no-non-null-assertion': 0,
|
'@typescript-eslint/no-non-null-assertion': 0,
|
||||||
|
// 'react/no-unknown-property': ['error', { ignore: ['css'] }],
|
||||||
// This setup is required to avoid a spam of errors when running eslint about React being
|
// This setup is required to avoid a spam of errors when running eslint about React being
|
||||||
// used before it is defined.
|
// used before it is defined.
|
||||||
//
|
//
|
||||||
|
|||||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1 +1,2 @@
|
|||||||
custom: [https://buy.stripe.com/14kdU99SI4UT7ni9AB, https://buy.stripe.com/14kaHXc0Q9b9372eUU]
|
github: pelican-dev
|
||||||
|
custom: [https://hub.pelican.dev/donors]
|
||||||
|
|||||||
24
.github/docker/default.conf
vendored
24
.github/docker/default.conf
vendored
@@ -4,6 +4,30 @@
|
|||||||
# If using CentOS this file should be placed in:
|
# If using CentOS this file should be placed in:
|
||||||
# /etc/nginx/conf.d/
|
# /etc/nginx/conf.d/
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
#
|
||||||
|
# Pterodactyl®
|
||||||
|
# Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|||||||
2
.github/docker/entrypoint.sh
vendored
2
.github/docker/entrypoint.sh
vendored
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/ash -e
|
#!/bin/ash -e
|
||||||
cd /app
|
cd /app
|
||||||
|
|
||||||
mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php7/ \
|
mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php8/ \
|
||||||
&& chmod 777 /var/log/panel/logs/ \
|
&& chmod 777 /var/log/panel/logs/ \
|
||||||
&& ln -s /app/storage/logs/ /var/log/panel/
|
&& ln -s /app/storage/logs/ /var/log/panel/
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18]
|
node-version: [18, 20]
|
||||||
steps:
|
steps:
|
||||||
- name: Code Checkout
|
- name: Code Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
86
.github/workflows/ci.yaml
vendored
86
.github/workflows/ci.yaml
vendored
@@ -6,8 +6,8 @@ on:
|
|||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
mysql:
|
||||||
name: Tests
|
name: MySQL
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -23,6 +23,21 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 3306
|
- 3306
|
||||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||||
|
env:
|
||||||
|
APP_ENV: testing
|
||||||
|
APP_DEBUG: "false"
|
||||||
|
APP_KEY: ThisIsARandomStringForTests12345
|
||||||
|
APP_TIMEZONE: UTC
|
||||||
|
APP_URL: http://localhost/
|
||||||
|
APP_ENVIRONMENT_ONLY: "true"
|
||||||
|
CACHE_DRIVER: array
|
||||||
|
MAIL_MAILER: array
|
||||||
|
SESSION_DRIVER: array
|
||||||
|
QUEUE_CONNECTION: sync
|
||||||
|
DB_CONNECTION: mysql
|
||||||
|
DB_HOST: 127.0.0.1
|
||||||
|
DB_DATABASE: testing
|
||||||
|
DB_USERNAME: root
|
||||||
steps:
|
steps:
|
||||||
- name: Code Checkout
|
- name: Code Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -48,18 +63,11 @@ jobs:
|
|||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- name: Setup .env
|
|
||||||
run: cp .env.example .env
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: composer install --no-interaction --no-suggest --prefer-dist
|
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||||
|
|
||||||
- name: Generate App Key
|
|
||||||
run: php artisan key:generate
|
|
||||||
|
|
||||||
- name: Unit tests
|
- name: Unit tests
|
||||||
run: vendor/bin/phpunit tests/Unit
|
run: vendor/bin/phpunit tests/Unit
|
||||||
if: ${{ always() }}
|
|
||||||
env:
|
env:
|
||||||
DB_HOST: UNIT_NO_DB
|
DB_HOST: UNIT_NO_DB
|
||||||
SKIP_MIGRATIONS: true
|
SKIP_MIGRATIONS: true
|
||||||
@@ -69,3 +77,63 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||||
DB_USERNAME: root
|
DB_USERNAME: root
|
||||||
|
|
||||||
|
sqlite:
|
||||||
|
name: SQLite
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: [8.2, 8.3]
|
||||||
|
env:
|
||||||
|
APP_ENV: testing
|
||||||
|
APP_DEBUG: "false"
|
||||||
|
APP_KEY: ThisIsARandomStringForTests12345
|
||||||
|
APP_TIMEZONE: UTC
|
||||||
|
APP_URL: http://localhost/
|
||||||
|
APP_ENVIRONMENT_ONLY: "true"
|
||||||
|
CACHE_DRIVER: array
|
||||||
|
MAIL_MAILER: array
|
||||||
|
SESSION_DRIVER: array
|
||||||
|
QUEUE_CONNECTION: sync
|
||||||
|
DB_CONNECTION: sqlite
|
||||||
|
DB_DATABASE: testing.sqlite
|
||||||
|
steps:
|
||||||
|
- name: Code Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get cache directory
|
||||||
|
id: composer-cache
|
||||||
|
run: |
|
||||||
|
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.composer-cache.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-${{ matrix.php }}-
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php }}
|
||||||
|
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||||
|
tools: composer:v2
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --no-suggest --prefer-dist
|
||||||
|
|
||||||
|
- name: Create SQLite file
|
||||||
|
run: touch database/testing.sqlite
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: vendor/bin/phpunit tests/Unit
|
||||||
|
env:
|
||||||
|
DB_HOST: UNIT_NO_DB
|
||||||
|
SKIP_MIGRATIONS: true
|
||||||
|
|
||||||
|
- name: Integration tests
|
||||||
|
run: vendor/bin/phpunit tests/Integration
|
||||||
|
|||||||
8
.github/workflows/cla.yaml
vendored
8
.github/workflows/cla.yaml
vendored
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: write
|
actions: write
|
||||||
contents: write
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
statuses: write
|
statuses: write
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: "CLA Assistant"
|
- name: "CLA Assistant"
|
||||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||||
uses: contributor-assistant/github-action@v2.3.0
|
uses: contributor-assistant/github-action@v2.4.0
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PERSONAL_ACCESS_TOKEN }}
|
||||||
with:
|
with:
|
||||||
path-to-signatures: 'version1/cla.json'
|
path-to-signatures: 'version1/cla.json'
|
||||||
path-to-document: 'https://github.com/pelican-dev/panel/blob/3.x/contributor_license_agreement.md'
|
path-to-document: 'https://github.com/pelican-dev/panel/blob/main/contributor_license_agreement.md'
|
||||||
branch: 'main'
|
branch: 'main'
|
||||||
allowlist: dependabot[bot]
|
allowlist: dependabot[bot]
|
||||||
remote-organization-name: pelican-dev
|
remote-organization-name: pelican-dev
|
||||||
|
|||||||
25
.github/workflows/release.yaml
vendored
25
.github/workflows/release.yaml
vendored
@@ -8,15 +8,18 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Code checkout
|
- name: Code checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -30,8 +33,8 @@ jobs:
|
|||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
run: |
|
run: |
|
||||||
BRANCH=release/${REF:10}
|
BRANCH=release/${REF:10}
|
||||||
git config --local user.email "ci@pelican.dev"
|
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --local user.name "Pelican CI"
|
git config --local user.name "github-actions[bot]"
|
||||||
git checkout -b $BRANCH
|
git checkout -b $BRANCH
|
||||||
git push -u origin $BRANCH
|
git push -u origin $BRANCH
|
||||||
sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:11}',/" config/app.php
|
sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:11}',/" config/app.php
|
||||||
@@ -44,16 +47,9 @@ jobs:
|
|||||||
rm -rf node_modules tests CODE_OF_CONDUCT.md CONTRIBUTING.md flake.lock flake.nix phpunit.xml shell.nix
|
rm -rf node_modules tests CODE_OF_CONDUCT.md CONTRIBUTING.md flake.lock flake.nix phpunit.xml shell.nix
|
||||||
tar -czf panel.tar.gz * .editorconfig .env.example .eslintignore .eslintrc.js .gitignore .prettierrc.json
|
tar -czf panel.tar.gz * .editorconfig .env.example .eslintignore .eslintrc.js .gitignore .prettierrc.json
|
||||||
|
|
||||||
- name: Extract changelog
|
- name: Create checksum
|
||||||
env:
|
|
||||||
REF: ${{ github.ref }}
|
|
||||||
run: |
|
|
||||||
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
|
|
||||||
|
|
||||||
- name: Create checksum and add to changelog
|
|
||||||
run: |
|
run: |
|
||||||
SUM=`sha256sum panel.tar.gz`
|
SUM=`sha256sum panel.tar.gz`
|
||||||
echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG
|
|
||||||
echo $SUM > checksum.txt
|
echo $SUM > checksum.txt
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
@@ -64,7 +60,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
|
||||||
body_path: ./RELEASE_CHANGELOG
|
|
||||||
|
|
||||||
- name: Upload release archive
|
- name: Upload release archive
|
||||||
id: upload-release-archive
|
id: upload-release-archive
|
||||||
|
|||||||
60
.gitignore
vendored
60
.gitignore
vendored
@@ -1,36 +1,28 @@
|
|||||||
/vendor
|
/.phpunit.cache
|
||||||
*.DS_Store*
|
/node_modules
|
||||||
!.env.ci
|
|
||||||
!.env.example
|
|
||||||
.env*
|
|
||||||
.vagrant/*
|
|
||||||
.vscode/*
|
|
||||||
storage/framework/*
|
|
||||||
/.idea
|
|
||||||
/nbproject
|
|
||||||
/.direnv
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
*.log
|
|
||||||
_ide_helper.php
|
|
||||||
_ide_helper_models.php
|
|
||||||
.phpstorm.meta.php
|
|
||||||
.yarn
|
|
||||||
public/assets/manifest.json
|
|
||||||
|
|
||||||
# For local development with docker
|
|
||||||
# Remove if we ever put the Dockerfile in the repo
|
|
||||||
.dockerignore
|
|
||||||
docker-compose.yml
|
|
||||||
|
|
||||||
# for image related files
|
|
||||||
misc
|
|
||||||
.php-cs-fixer.cache
|
|
||||||
coverage.xml
|
|
||||||
resources/lang/locales.js
|
|
||||||
.phpunit.result.cache
|
|
||||||
|
|
||||||
/public/build
|
/public/build
|
||||||
/public/hot
|
/public/hot
|
||||||
result
|
/public/storage
|
||||||
docker-compose.yaml
|
/storage/*.key
|
||||||
|
/storage/clockwork/*
|
||||||
|
/vendor
|
||||||
|
*.DS_Store*
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
auth.json
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
|
||||||
|
public/assets/manifest.json
|
||||||
|
/database/*.sqlite
|
||||||
|
filament-monaco-editor/
|
||||||
|
_ide_helper*
|
||||||
|
/.phpstorm.meta.php
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Build the assets that are needed for the frontend. This build stage is then discarded
|
# Build the assets that are needed for the frontend. This build stage is then discarded
|
||||||
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
|
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
|
||||||
# level distribution
|
# level distribution
|
||||||
FROM --platform=$TARGETOS/$TARGETARCH mhart/alpine-node:14
|
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN yarn install --frozen-lockfile \
|
RUN yarn install --frozen-lockfile \
|
||||||
@@ -10,13 +10,13 @@ RUN yarn install --frozen-lockfile \
|
|||||||
|
|
||||||
# Stage 1:
|
# Stage 1:
|
||||||
# Build the actual container with all of the needed PHP dependencies that will run the application.
|
# Build the actual container with all of the needed PHP dependencies that will run the application.
|
||||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.1-fpm-alpine
|
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . ./
|
COPY . ./
|
||||||
COPY --from=0 /app/public/assets ./public/assets
|
COPY --from=0 /app/public/assets ./public/assets
|
||||||
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev certbot certbot-nginx \
|
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev certbot certbot-nginx \
|
||||||
&& docker-php-ext-configure zip \
|
&& docker-php-ext-configure zip \
|
||||||
&& docker-php-ext-install bcmath gd pdo_mysql zip \
|
&& docker-php-ext-install bcmath gd intl pdo_mysql zip \
|
||||||
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
||||||
&& cp .env.example .env \
|
&& cp .env.example .env \
|
||||||
&& mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache \
|
&& mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache \
|
||||||
|
|||||||
@@ -5,38 +5,34 @@ namespace App\Console\Commands\Environment;
|
|||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Contracts\Console\Kernel;
|
use Illuminate\Contracts\Console\Kernel;
|
||||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
class AppSettingsCommand extends Command
|
class AppSettingsCommand extends Command
|
||||||
{
|
{
|
||||||
use EnvironmentWriterTrait;
|
use EnvironmentWriterTrait;
|
||||||
|
|
||||||
public const CACHE_DRIVERS = [
|
public const CACHE_DRIVERS = [
|
||||||
'redis' => 'Redis',
|
|
||||||
'memcached' => 'Memcached',
|
|
||||||
'file' => 'Filesystem (recommended)',
|
'file' => 'Filesystem (recommended)',
|
||||||
|
'redis' => 'Redis',
|
||||||
];
|
];
|
||||||
|
|
||||||
public const SESSION_DRIVERS = [
|
public const SESSION_DRIVERS = [
|
||||||
'redis' => 'Redis',
|
|
||||||
'memcached' => 'Memcached',
|
|
||||||
'database' => 'MySQL Database',
|
|
||||||
'file' => 'Filesystem (recommended)',
|
'file' => 'Filesystem (recommended)',
|
||||||
|
'redis' => 'Redis',
|
||||||
|
'database' => 'Database',
|
||||||
'cookie' => 'Cookie',
|
'cookie' => 'Cookie',
|
||||||
];
|
];
|
||||||
|
|
||||||
public const QUEUE_DRIVERS = [
|
public const QUEUE_DRIVERS = [
|
||||||
|
'database' => 'Database (recommended)',
|
||||||
'redis' => 'Redis',
|
'redis' => 'Redis',
|
||||||
'database' => 'MySQL Database',
|
'sync' => 'Synchronous',
|
||||||
'sync' => 'Sync (recommended)',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $description = 'Configure basic environment settings for the Panel.';
|
protected $description = 'Configure basic environment settings for the Panel.';
|
||||||
|
|
||||||
protected $signature = 'p:environment:setup
|
protected $signature = 'p:environment:setup
|
||||||
{--new-salt : Whether or not to generate a new salt for Hashids.}
|
|
||||||
{--author= : The email that services created on this instance should be linked to.}
|
|
||||||
{--url= : The URL that this Panel is running on.}
|
{--url= : The URL that this Panel is running on.}
|
||||||
{--timezone= : The timezone to use for Panel times.}
|
|
||||||
{--cache= : The cache driver backend to use.}
|
{--cache= : The cache driver backend to use.}
|
||||||
{--session= : The session driver backend to use.}
|
{--session= : The session driver backend to use.}
|
||||||
{--queue= : The queue driver backend to use.}
|
{--queue= : The queue driver backend to use.}
|
||||||
@@ -62,35 +58,14 @@ class AppSettingsCommand extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
if (empty(config('hashids.salt')) || $this->option('new-salt')) {
|
$this->variables['APP_TIMEZONE'] = 'UTC';
|
||||||
$this->variables['HASHIDS_SALT'] = str_random(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->output->comment('Provide the email address that eggs exported by this Panel should be from. This should be a valid email address.');
|
$this->output->comment(__('commands.appsettings.comment.url'));
|
||||||
$this->variables['APP_SERVICE_AUTHOR'] = $this->option('author') ?? $this->ask(
|
|
||||||
'Egg Author Email',
|
|
||||||
config('panel.service.author', 'unknown@unknown.com')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!filter_var($this->variables['APP_SERVICE_AUTHOR'], FILTER_VALIDATE_EMAIL)) {
|
|
||||||
$this->output->error('The service author email provided is invalid.');
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->output->comment('The application URL MUST begin with https:// or http:// depending on if you are using SSL or not. If you do not include the scheme your emails and other content will link to the wrong location.');
|
|
||||||
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
|
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
|
||||||
'Application URL',
|
'Application URL',
|
||||||
config('app.url', 'https://example.com')
|
config('app.url', 'https://example.com')
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->output->comment('The timezone should match one of PHP\'s supported timezones. If you are unsure, please reference https://php.net/manual/en/timezones.php.');
|
|
||||||
$this->variables['APP_TIMEZONE'] = $this->option('timezone') ?? $this->anticipate(
|
|
||||||
'Application Timezone',
|
|
||||||
\DateTimeZone::listIdentifiers(),
|
|
||||||
config('app.timezone')
|
|
||||||
);
|
|
||||||
|
|
||||||
$selected = config('cache.default', 'file');
|
$selected = config('cache.default', 'file');
|
||||||
$this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice(
|
$this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice(
|
||||||
'Cache Driver',
|
'Cache Driver',
|
||||||
@@ -105,7 +80,7 @@ class AppSettingsCommand extends Command
|
|||||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||||
);
|
);
|
||||||
|
|
||||||
$selected = config('queue.default', 'sync');
|
$selected = config('queue.default', 'database');
|
||||||
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
||||||
'Queue Driver',
|
'Queue Driver',
|
||||||
self::QUEUE_DRIVERS,
|
self::QUEUE_DRIVERS,
|
||||||
@@ -115,7 +90,7 @@ class AppSettingsCommand extends Command
|
|||||||
if (!is_null($this->option('settings-ui'))) {
|
if (!is_null($this->option('settings-ui'))) {
|
||||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true';
|
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true';
|
||||||
} else {
|
} else {
|
||||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm('Enable UI based settings editor?', true) ? 'false' : 'true';
|
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure session cookies are set as "secure" when using HTTPS
|
// Make sure session cookies are set as "secure" when using HTTPS
|
||||||
@@ -123,29 +98,40 @@ class AppSettingsCommand extends Command
|
|||||||
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
|
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkForRedis();
|
$redisUsed = count(collect($this->variables)->filter(function ($item) {
|
||||||
|
return $item === 'redis';
|
||||||
|
})) !== 0;
|
||||||
|
|
||||||
|
if ($redisUsed) {
|
||||||
|
$this->requestRedisSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = base_path('.env');
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
copy($path . '.example', $path);
|
||||||
|
}
|
||||||
|
|
||||||
$this->writeToEnvironment($this->variables);
|
$this->writeToEnvironment($this->variables);
|
||||||
|
|
||||||
|
if (!config('app.key')) {
|
||||||
|
Artisan::call('key:generate');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
|
||||||
|
Artisan::call('p:environment:queue-service', $redisUsed ? ['--use-redis'] : []);
|
||||||
|
}
|
||||||
|
|
||||||
$this->info($this->console->output());
|
$this->info($this->console->output());
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if redis is selected, if so, request connection details and verify them.
|
* Request connection details and verify them.
|
||||||
*/
|
*/
|
||||||
private function checkForRedis()
|
private function requestRedisSettings(): void
|
||||||
{
|
{
|
||||||
$items = collect($this->variables)->filter(function ($item) {
|
$this->output->note(__('commands.appsettings.redis.note'));
|
||||||
return $item === 'redis';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Redis was not selected, no need to continue.
|
|
||||||
if (count($items) === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->output->note('You\'ve selected the Redis driver for one or more options, please provide valid connection information below. In most cases you can use the defaults provided unless you have modified your setup.');
|
|
||||||
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
|
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
|
||||||
'Redis Host',
|
'Redis Host',
|
||||||
config('database.redis.default.host')
|
config('database.redis.default.host')
|
||||||
@@ -158,7 +144,7 @@ class AppSettingsCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($askForRedisPassword) {
|
if ($askForRedisPassword) {
|
||||||
$this->output->comment('By default a Redis server instance has no password as it is running locally and inaccessible to the outside world. If this is the case, simply hit enter without entering a value.');
|
$this->output->comment(__('commands.appsettings.redis.comment'));
|
||||||
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
|
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
|
||||||
'Redis Password'
|
'Redis Password'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,14 +11,20 @@ class DatabaseSettingsCommand extends Command
|
|||||||
{
|
{
|
||||||
use EnvironmentWriterTrait;
|
use EnvironmentWriterTrait;
|
||||||
|
|
||||||
|
public const DATABASE_DRIVERS = [
|
||||||
|
'sqlite' => 'SQLite (recommended)',
|
||||||
|
'mysql' => 'MySQL',
|
||||||
|
];
|
||||||
|
|
||||||
protected $description = 'Configure database settings for the Panel.';
|
protected $description = 'Configure database settings for the Panel.';
|
||||||
|
|
||||||
protected $signature = 'p:environment:database
|
protected $signature = 'p:environment:database
|
||||||
|
{--driver= : The database driver backend to use.}
|
||||||
|
{--database= : The database to use.}
|
||||||
{--host= : The connection address for the MySQL server.}
|
{--host= : The connection address for the MySQL server.}
|
||||||
{--port= : The connection port for the MySQL server.}
|
{--port= : The connection port for the MySQL server.}
|
||||||
{--database= : The database to use.}
|
{--username= : Username to use when connecting to the MySQL server.}
|
||||||
{--username= : Username to use when connecting.}
|
{--password= : Password to use for the MySQL database.}';
|
||||||
{--password= : Password to use for this database.}';
|
|
||||||
|
|
||||||
protected array $variables = [];
|
protected array $variables = [];
|
||||||
|
|
||||||
@@ -35,51 +41,65 @@ class DatabaseSettingsCommand extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$this->output->note('It is highly recommended to not use "localhost" as your database host as we have seen frequent socket connection issues. If you want to use a local connection you should be using "127.0.0.1".');
|
$selected = config('database.default', 'sqlite');
|
||||||
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
|
$this->variables['DB_CONNECTION'] = $this->option('driver') ?? $this->choice(
|
||||||
'Database Host',
|
'Database Driver',
|
||||||
config('database.connections.mysql.host', '127.0.0.1')
|
self::DATABASE_DRIVERS,
|
||||||
|
array_key_exists($selected, self::DATABASE_DRIVERS) ? $selected : null
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->variables['DB_PORT'] = $this->option('port') ?? $this->ask(
|
if ($this->variables['DB_CONNECTION'] === 'mysql') {
|
||||||
'Database Port',
|
$this->output->note(__('commands.database_settings.DB_HOST_note'));
|
||||||
config('database.connections.mysql.port', 3306)
|
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
|
||||||
);
|
'Database Host',
|
||||||
|
config('database.connections.mysql.host', '127.0.0.1')
|
||||||
|
);
|
||||||
|
|
||||||
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
$this->variables['DB_PORT'] = $this->option('port') ?? $this->ask(
|
||||||
'Database Name',
|
'Database Port',
|
||||||
config('database.connections.mysql.database', 'panel')
|
config('database.connections.mysql.port', 3306)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->output->note('Using the "root" account for MySQL connections is not only highly frowned upon, it is also not allowed by this application. You\'ll need to have created a MySQL user for this software.');
|
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||||
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
|
'Database Name',
|
||||||
'Database Username',
|
config('database.connections.mysql.database', 'panel')
|
||||||
config('database.connections.mysql.username', 'panel')
|
);
|
||||||
);
|
|
||||||
|
|
||||||
$askForMySQLPassword = true;
|
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
|
||||||
if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) {
|
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
|
||||||
$this->variables['DB_PASSWORD'] = config('database.connections.mysql.password');
|
'Database Username',
|
||||||
$askForMySQLPassword = $this->confirm('It appears you already have a MySQL connection password defined, would you like to change it?');
|
config('database.connections.mysql.username', 'pelican')
|
||||||
}
|
);
|
||||||
|
|
||||||
if ($askForMySQLPassword) {
|
$askForMySQLPassword = true;
|
||||||
$this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password');
|
if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) {
|
||||||
}
|
$this->variables['DB_PASSWORD'] = config('database.connections.mysql.password');
|
||||||
|
$askForMySQLPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
|
||||||
try {
|
|
||||||
$this->testMySQLConnection();
|
|
||||||
} catch (\PDOException $exception) {
|
|
||||||
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
|
||||||
$this->output->error('Your connection credentials have NOT been saved. You will need to provide valid connection information before proceeding.');
|
|
||||||
|
|
||||||
if ($this->confirm('Go back and try again?')) {
|
|
||||||
$this->database->disconnect('_panel_command_test');
|
|
||||||
|
|
||||||
return $this->handle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
if ($askForMySQLPassword) {
|
||||||
|
$this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->testMySQLConnection();
|
||||||
|
} catch (\PDOException $exception) {
|
||||||
|
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
||||||
|
$this->output->error(__('commands.database_settings.DB_error_2'));
|
||||||
|
|
||||||
|
if ($this->confirm(__('commands.database_settings.go_back'))) {
|
||||||
|
$this->database->disconnect('_panel_command_test');
|
||||||
|
|
||||||
|
return $this->handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
|
||||||
|
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||||
|
'Database Path',
|
||||||
|
config('database.connections.sqlite.database', database_path('database.sqlite'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->writeToEnvironment($this->variables);
|
$this->writeToEnvironment($this->variables);
|
||||||
|
|||||||
@@ -31,19 +31,20 @@ class EmailSettingsCommand extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$this->variables['MAIL_DRIVER'] = $this->option('driver') ?? $this->choice(
|
$this->variables['MAIL_MAILER'] = $this->option('driver') ?? $this->choice(
|
||||||
trans('command/messages.environment.mail.ask_driver'),
|
trans('command/messages.environment.mail.ask_driver'),
|
||||||
[
|
[
|
||||||
|
'log' => 'Log',
|
||||||
'smtp' => 'SMTP Server',
|
'smtp' => 'SMTP Server',
|
||||||
'sendmail' => 'sendmail Binary',
|
'sendmail' => 'sendmail Binary',
|
||||||
'mailgun' => 'Mailgun Transactional Email',
|
'mailgun' => 'Mailgun',
|
||||||
'mandrill' => 'Mandrill Transactional Email',
|
'mandrill' => 'Mandrill',
|
||||||
'postmark' => 'Postmark Transactional Email',
|
'postmark' => 'Postmark',
|
||||||
],
|
],
|
||||||
config('mail.default', 'smtp')
|
env('MAIL_MAILER', env('MAIL_DRIVER', 'smtp')),
|
||||||
);
|
);
|
||||||
|
|
||||||
$method = 'setup' . studly_case($this->variables['MAIL_DRIVER']) . 'DriverVariables';
|
$method = 'setup' . studly_case($this->variables['MAIL_MAILER']) . 'DriverVariables';
|
||||||
if (method_exists($this, $method)) {
|
if (method_exists($this, $method)) {
|
||||||
$this->{$method}();
|
$this->{$method}();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Environment;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
|
||||||
|
class QueueWorkerServiceCommand extends Command
|
||||||
|
{
|
||||||
|
protected $description = 'Create the service for the queue worker.';
|
||||||
|
|
||||||
|
protected $signature = 'p:environment:queue-service
|
||||||
|
{--service-name= : Name of the queue worker service.}
|
||||||
|
{--user= : The user that PHP runs under.}
|
||||||
|
{--group= : The group that PHP runs under.}
|
||||||
|
{--use-redis : Whether redis is used.}
|
||||||
|
{--overwrite : Force overwrite if the service file already exists.}';
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$serviceName = $this->option('service-name') ?? $this->ask('Service name', 'pelican-queue');
|
||||||
|
$path = '/etc/systemd/system/' . $serviceName . '.service';
|
||||||
|
|
||||||
|
if (file_exists($path) && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
|
||||||
|
$this->line('Creation of queue worker service file aborted.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->option('user') ?? $this->ask('User', 'www-data');
|
||||||
|
$group = $this->option('group') ?? $this->ask('Group', 'www-data');
|
||||||
|
|
||||||
|
$afterRedis = $this->option('use-redis') ? '\nAfter=redis-server.service' : '';
|
||||||
|
|
||||||
|
$basePath = base_path();
|
||||||
|
|
||||||
|
$success = File::put($path, "# Pelican Queue File
|
||||||
|
# ----------------------------------
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Pelican Queue Service$afterRedis
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=$user
|
||||||
|
Group=$group
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/bin/php $basePath/artisan queue:work --queue=high,standard,low --tries=3
|
||||||
|
StartLimitInterval=180
|
||||||
|
StartLimitBurst=30
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
");
|
||||||
|
|
||||||
|
if (!$success) {
|
||||||
|
$this->error('Error creating service file');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Process::run("systemctl enable --now $serviceName.service");
|
||||||
|
if ($result->failed()) {
|
||||||
|
$this->error('Error enabling service: ' . $result->errorOutput());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->line('Queue worker service file created successfully.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@ class InfoCommand extends Command
|
|||||||
['Panel Version', config('app.version')],
|
['Panel Version', config('app.version')],
|
||||||
['Latest Version', $this->versionService->getPanel()],
|
['Latest Version', $this->versionService->getPanel()],
|
||||||
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
|
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
|
||||||
['Unique Identifier', config('panel.service.author')],
|
|
||||||
], 'compact');
|
], 'compact');
|
||||||
|
|
||||||
$this->output->title('Application Configuration');
|
$this->output->title('Application Configuration');
|
||||||
@@ -38,13 +37,11 @@ class InfoCommand extends Command
|
|||||||
['Debug Mode', $this->formatText(config('app.debug') ? 'Yes' : 'No', !config('app.debug') ?: 'bg=red')],
|
['Debug Mode', $this->formatText(config('app.debug') ? 'Yes' : 'No', !config('app.debug') ?: 'bg=red')],
|
||||||
['Installation URL', config('app.url')],
|
['Installation URL', config('app.url')],
|
||||||
['Installation Directory', base_path()],
|
['Installation Directory', base_path()],
|
||||||
['Timezone', config('app.timezone')],
|
|
||||||
['Cache Driver', config('cache.default')],
|
['Cache Driver', config('cache.default')],
|
||||||
['Queue Driver', config('queue.default')],
|
['Queue Driver', config('queue.default')],
|
||||||
['Session Driver', config('session.driver')],
|
['Session Driver', config('session.driver')],
|
||||||
['Filesystem Driver', config('filesystems.default')],
|
['Filesystem Driver', config('filesystems.default')],
|
||||||
['Default Theme', config('themes.active')],
|
['Default Theme', config('themes.active')],
|
||||||
['Proxies', config('trustedproxies.proxies')],
|
|
||||||
], 'compact');
|
], 'compact');
|
||||||
|
|
||||||
$this->output->title('Database Configuration');
|
$this->output->title('Database Configuration');
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class MakeNodeCommand extends Command
|
|||||||
{--overallocateMemory= : Enter the amount of ram to overallocate (% or -1 to overallocate the maximum).}
|
{--overallocateMemory= : Enter the amount of ram to overallocate (% or -1 to overallocate the maximum).}
|
||||||
{--maxDisk= : Set the max disk amount.}
|
{--maxDisk= : Set the max disk amount.}
|
||||||
{--overallocateDisk= : Enter the amount of disk to overallocate (% or -1 to overallocate the maximum).}
|
{--overallocateDisk= : Enter the amount of disk to overallocate (% or -1 to overallocate the maximum).}
|
||||||
|
{--maxCpu= : Set the max cpu amount.}
|
||||||
|
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
|
||||||
{--uploadSize= : Enter the maximum upload filesize.}
|
{--uploadSize= : Enter the maximum upload filesize.}
|
||||||
{--daemonListeningPort= : Enter the daemon listening port.}
|
{--daemonListeningPort= : Enter the daemon listening port.}
|
||||||
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
|
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
|
||||||
@@ -42,27 +44,30 @@ class MakeNodeCommand extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$data['name'] = $this->option('name') ?? $this->ask('Enter a short identifier used to distinguish this node from others');
|
$data['name'] = $this->option('name') ?? $this->ask(__('commands.make_node.name'));
|
||||||
$data['description'] = $this->option('description') ?? $this->ask('Enter a description to identify the node');
|
$data['description'] = $this->option('description') ?? $this->ask(__('commands.make_node.description'));
|
||||||
$data['scheme'] = $this->option('scheme') ?? $this->anticipate(
|
$data['scheme'] = $this->option('scheme') ?? $this->anticipate(
|
||||||
'Please either enter https for SSL or http for a non-ssl connection',
|
__('commands.make_node.scheme'),
|
||||||
['https', 'http'],
|
['https', 'http'],
|
||||||
'https'
|
'https'
|
||||||
);
|
);
|
||||||
$data['fqdn'] = $this->option('fqdn') ?? $this->ask('Enter a domain name (e.g node.example.com) to be used for connecting to the daemon. An IP address may only be used if you are not using SSL for this node');
|
|
||||||
$data['public'] = $this->option('public') ?? $this->confirm('Should this node be public? As a note, setting a node to private you will be denying the ability to auto-deploy to this node.', true);
|
$data['fqdn'] = $this->option('fqdn') ?? $this->ask(__('commands.make_node.fqdn'));
|
||||||
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm('Is your FQDN behind a proxy?');
|
$data['public'] = $this->option('public') ?? $this->confirm(__('commands.make_node.public'), true);
|
||||||
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm('Should maintenance mode be enabled?');
|
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm(__('commands.make_node.behind_proxy'));
|
||||||
$data['memory'] = $this->option('maxMemory') ?? $this->ask('Enter the maximum amount of memory');
|
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm(__('commands.make_node.maintenance_mode'));
|
||||||
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask('Enter the amount of memory to over allocate by, -1 will disable checking and 0 will prevent creating new servers');
|
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'));
|
||||||
$data['disk'] = $this->option('maxDisk') ?? $this->ask('Enter the maximum amount of disk space');
|
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'));
|
||||||
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask('Enter the amount of memory to over allocate by, -1 will disable checking and 0 will prevent creating new server');
|
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'));
|
||||||
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask('Enter the maximum filesize upload', '100');
|
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'));
|
||||||
$data['daemonListen'] = $this->option('daemonListeningPort') ?? $this->ask('Enter the daemon listening port', '8080');
|
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'));
|
||||||
$data['daemonSFTP'] = $this->option('daemonSFTPPort') ?? $this->ask('Enter the daemon SFTP listening port', '2022');
|
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'));
|
||||||
$data['daemonBase'] = $this->option('daemonBase') ?? $this->ask('Enter the base folder', '/var/lib/panel/volumes');
|
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '100');
|
||||||
|
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
|
||||||
|
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');
|
||||||
|
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
|
||||||
|
|
||||||
$node = $this->creationService->handle($data);
|
$node = $this->creationService->handle($data);
|
||||||
$this->line('Successfully created a new node with the name ' . $data['name'] . ' and has an id of ' . $node->id . '.');
|
$this->line(__('commands.make_node.succes1') . $data['name'] . __('commands.make_node.succes2') . $node->id . '.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ class NodeConfigurationCommand extends Command
|
|||||||
|
|
||||||
/** @var \App\Models\Node $node */
|
/** @var \App\Models\Node $node */
|
||||||
$node = Node::query()->where($column, $this->argument('node'))->firstOr(function () {
|
$node = Node::query()->where($column, $this->argument('node'))->firstOr(function () {
|
||||||
$this->error('The selected node does not exist.');
|
$this->error(__('commands.node_config.error_not_exist'));
|
||||||
|
|
||||||
exit(1);
|
exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
$format = $this->option('format');
|
$format = $this->option('format');
|
||||||
if (!in_array($format, ['yaml', 'yml', 'json'])) {
|
if (!in_array($format, ['yaml', 'yml', 'json'])) {
|
||||||
$this->error('Invalid format specified. Valid options are "yaml" and "json".');
|
$this->error(__('commands.node_config.error_invalid_format'));
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ class KeyGenerateCommand extends BaseKeyGenerateCommand
|
|||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
if (!empty(config('app.key')) && $this->input->isInteractive()) {
|
if (!empty(config('app.key')) && $this->input->isInteractive()) {
|
||||||
$this->output->warning('It appears you have already configured an application encryption key. Continuing with this process with overwrite that key and cause data corruption for any existing encrypted data. DO NOT CONTINUE UNLESS YOU KNOW WHAT YOU ARE DOING.');
|
$this->output->warning(__('commands.key_generate.error_already_exist'));
|
||||||
if (!$this->confirm('I understand the consequences of performing this command and accept all responsibility for the loss of encrypted data.')) {
|
if (!$this->confirm(__('commands.key_generate.understand'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->confirm('Are you sure you wish to continue? Changing the application encryption key WILL CAUSE DATA LOSS.')) {
|
if (!$this->confirm(__('commands.key_generate.continue'))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use Illuminate\Console\Command;
|
|||||||
use App\Models\Schedule;
|
use App\Models\Schedule;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use App\Services\Schedules\ProcessScheduleService;
|
use App\Services\Schedules\ProcessScheduleService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class ProcessRunnableCommand extends Command
|
class ProcessRunnableCommand extends Command
|
||||||
{
|
{
|
||||||
@@ -23,11 +24,11 @@ class ProcessRunnableCommand extends Command
|
|||||||
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
|
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->where('is_processing', false)
|
->where('is_processing', false)
|
||||||
->whereRaw('next_run_at <= NOW()')
|
->whereDate('next_run_at', '<=', Carbon::now()->toDateString())
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
if ($schedules->count() < 1) {
|
if ($schedules->count() < 1) {
|
||||||
$this->line('There are no scheduled tasks for servers that need to be run.');
|
$this->line(__('commands.schedule.process.no_tasks'));
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -61,12 +62,12 @@ class ProcessRunnableCommand extends Command
|
|||||||
|
|
||||||
$this->line(trans('command/messages.schedule.output_line', [
|
$this->line(trans('command/messages.schedule.output_line', [
|
||||||
'schedule' => $schedule->name,
|
'schedule' => $schedule->name,
|
||||||
'hash' => $schedule->hashid,
|
'id' => $schedule->id,
|
||||||
]));
|
]));
|
||||||
} catch (\Throwable|\Exception $exception) {
|
} catch (\Throwable|\Exception $exception) {
|
||||||
logger()->error($exception, ['schedule_id' => $schedule->id]);
|
logger()->error($exception, ['schedule_id' => $schedule->id]);
|
||||||
|
|
||||||
$this->error("An error was encountered while processing Schedule #$schedule->id: " . $exception->getMessage());
|
$this->error(__('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,29 +34,30 @@ class UpgradeCommand extends Command
|
|||||||
{
|
{
|
||||||
$skipDownload = $this->option('skip-download');
|
$skipDownload = $this->option('skip-download');
|
||||||
if (!$skipDownload) {
|
if (!$skipDownload) {
|
||||||
$this->output->warning('This command does not verify the integrity of downloaded assets. Please ensure that you trust the download source before continuing. If you do not wish to download an archive, please indicate that using the --skip-download flag, or answering "no" to the question below.');
|
$this->output->warning(__('commands.upgrade.integrity'));
|
||||||
$this->output->comment('Download Source (set with --url=):');
|
$this->output->comment(__('commands.upgrade.source_url'));
|
||||||
$this->line($this->getUrl());
|
$this->line($this->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version_compare(PHP_VERSION, '7.4.0') < 0) {
|
if (version_compare(PHP_VERSION, '7.4.0') < 0) {
|
||||||
$this->error('Cannot execute self-upgrade process. The minimum required PHP version required is 7.4.0, you have [' . PHP_VERSION . '].');
|
$this->error(__('commands.upgrade.php_version') . ' [' . PHP_VERSION . '].');
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = 'www-data';
|
$user = 'www-data';
|
||||||
$group = 'www-data';
|
$group = 'www-data';
|
||||||
if ($this->input->isInteractive()) {
|
if ($this->input->isInteractive()) {
|
||||||
if (!$skipDownload) {
|
if (!$skipDownload) {
|
||||||
$skipDownload = !$this->confirm('Would you like to download and unpack the archive files for the latest version?', true);
|
$skipDownload = !$this->confirm(__('commands.upgrade.skipDownload'), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_null($this->option('user'))) {
|
if (is_null($this->option('user'))) {
|
||||||
$userDetails = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner('public')) : [];
|
$userDetails = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner('public')) : [];
|
||||||
$user = $userDetails['name'] ?? 'www-data';
|
$user = $userDetails['name'] ?? 'www-data';
|
||||||
|
|
||||||
if (!$this->confirm("Your webserver user has been detected as <fg=blue>[{$user}]:</> is this correct?", true)) {
|
$message = __('commands.upgrade.webserver_user', ['user' => $user]);
|
||||||
|
if (!$this->confirm($message, true)) {
|
||||||
$user = $this->anticipate(
|
$user = $this->anticipate(
|
||||||
'Please enter the name of the user running your webserver process. This varies from system to system, but is generally "www-data", "nginx", or "apache".',
|
__('commands.upgrade.name_webserver'),
|
||||||
[
|
[
|
||||||
'www-data',
|
'www-data',
|
||||||
'nginx',
|
'nginx',
|
||||||
@@ -70,9 +71,10 @@ class UpgradeCommand extends Command
|
|||||||
$groupDetails = function_exists('posix_getgrgid') ? posix_getgrgid(filegroup('public')) : [];
|
$groupDetails = function_exists('posix_getgrgid') ? posix_getgrgid(filegroup('public')) : [];
|
||||||
$group = $groupDetails['name'] ?? 'www-data';
|
$group = $groupDetails['name'] ?? 'www-data';
|
||||||
|
|
||||||
if (!$this->confirm("Your webserver group has been detected as <fg=blue>[{$group}]:</> is this correct?", true)) {
|
$message = __('commands.upgrade.group_webserver', ['group' => $user]);
|
||||||
|
if (!$this->confirm($message, true)) {
|
||||||
$group = $this->anticipate(
|
$group = $this->anticipate(
|
||||||
'Please enter the name of the group running your webserver process. Normally this is the same as your user.',
|
__('commands.upgrade.group_webserver_question'),
|
||||||
[
|
[
|
||||||
'www-data',
|
'www-data',
|
||||||
'nginx',
|
'nginx',
|
||||||
@@ -82,8 +84,8 @@ class UpgradeCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->confirm('Are you sure you want to run the upgrade process for your Panel?')) {
|
if (!$this->confirm(__('commands.upgrade.are_your_sure'))) {
|
||||||
$this->warn('Upgrade process terminated by user.');
|
$this->warn(__('commands.upgrade.terminated'));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -173,7 +175,7 @@ class UpgradeCommand extends Command
|
|||||||
});
|
});
|
||||||
|
|
||||||
$this->newLine(2);
|
$this->newLine(2);
|
||||||
$this->info('Panel has been successfully upgraded. Please ensure you also update any Daemon instances');
|
$this->info(__('commands.upgrade.success'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function withProgress(ProgressBar $bar, \Closure $callback)
|
protected function withProgress(ProgressBar $bar, \Closure $callback)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class MakeUserCommand extends Command
|
|||||||
{
|
{
|
||||||
protected $description = 'Creates a user on the system via the CLI.';
|
protected $description = 'Creates a user on the system via the CLI.';
|
||||||
|
|
||||||
protected $signature = 'p:user:make {--email=} {--username=} {--name-first=} {--name-last=} {--password=} {--admin=} {--no-password}';
|
protected $signature = 'p:user:make {--email=} {--username=} {--password=} {--admin=} {--no-password}';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MakeUserCommand constructor.
|
* MakeUserCommand constructor.
|
||||||
@@ -40,8 +40,6 @@ class MakeUserCommand extends Command
|
|||||||
$root_admin = $this->option('admin') ?? $this->confirm(trans('command/messages.user.ask_admin'));
|
$root_admin = $this->option('admin') ?? $this->confirm(trans('command/messages.user.ask_admin'));
|
||||||
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));
|
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));
|
||||||
$username = $this->option('username') ?? $this->ask(trans('command/messages.user.ask_username'));
|
$username = $this->option('username') ?? $this->ask(trans('command/messages.user.ask_username'));
|
||||||
$name_first = $this->option('name-first') ?? $this->ask(trans('command/messages.user.ask_name_first'));
|
|
||||||
$name_last = $this->option('name-last') ?? $this->ask(trans('command/messages.user.ask_name_last'));
|
|
||||||
|
|
||||||
if (is_null($password = $this->option('password')) && !$this->option('no-password')) {
|
if (is_null($password = $this->option('password')) && !$this->option('no-password')) {
|
||||||
$this->warn(trans('command/messages.user.ask_password_help'));
|
$this->warn(trans('command/messages.user.ask_password_help'));
|
||||||
@@ -49,12 +47,11 @@ class MakeUserCommand extends Command
|
|||||||
$password = $this->secret(trans('command/messages.user.ask_password'));
|
$password = $this->secret(trans('command/messages.user.ask_password'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $this->creationService->handle(compact('email', 'username', 'name_first', 'name_last', 'password', 'root_admin'));
|
$user = $this->creationService->handle(compact('email', 'username', 'password', 'root_admin'));
|
||||||
$this->table(['Field', 'Value'], [
|
$this->table(['Field', 'Value'], [
|
||||||
['UUID', $user->uuid],
|
['UUID', $user->uuid],
|
||||||
['Email', $user->email],
|
['Email', $user->email],
|
||||||
['Username', $user->username],
|
['Username', $user->username],
|
||||||
['Name', $user->name],
|
|
||||||
['Admin', $user->root_admin ? 'Yes' : 'No'],
|
['Admin', $user->root_admin ? 'Yes' : 'No'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Contracts\Extensions;
|
|
||||||
|
|
||||||
use Hashids\HashidsInterface as VendorHashidsInterface;
|
|
||||||
|
|
||||||
interface HashidsInterface extends VendorHashidsInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Decode an encoded hashid and return the first result.
|
|
||||||
*
|
|
||||||
* @throws \InvalidArgumentException
|
|
||||||
*/
|
|
||||||
public function decodeFirst(string $encoded, string $default = null): mixed;
|
|
||||||
}
|
|
||||||
46
app/Enums/ContainerStatus.php
Normal file
46
app/Enums/ContainerStatus.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum ContainerStatus: string
|
||||||
|
{
|
||||||
|
// Docker Based
|
||||||
|
case Created = 'created';
|
||||||
|
case Running = 'running';
|
||||||
|
case Restarting = 'restarting';
|
||||||
|
case Exited = 'exited';
|
||||||
|
case Paused = 'paused';
|
||||||
|
case Dead = 'dead';
|
||||||
|
case Removing = 'removing';
|
||||||
|
|
||||||
|
// HTTP Based
|
||||||
|
case Missing = 'missing';
|
||||||
|
|
||||||
|
public function icon(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Created => 'tabler-heart-plus',
|
||||||
|
self::Running => 'tabler-heartbeat',
|
||||||
|
self::Restarting => 'tabler-heart-bolt',
|
||||||
|
self::Exited => 'tabler-heart-exclamation',
|
||||||
|
self::Paused => 'tabler-heart-pause',
|
||||||
|
self::Dead => 'tabler-heart-x',
|
||||||
|
self::Removing => 'tabler-heart-down',
|
||||||
|
self::Missing => 'tabler-heart-question',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function color(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Created => 'primary',
|
||||||
|
self::Running => 'success',
|
||||||
|
self::Restarting => 'info',
|
||||||
|
self::Exited => 'danger',
|
||||||
|
self::Paused => 'warning',
|
||||||
|
self::Dead => 'danger',
|
||||||
|
self::Removing => 'warning',
|
||||||
|
self::Missing => 'danger',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Enums/HttpStatusCode.php
Normal file
31
app/Enums/HttpStatusCode.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum HttpStatusCode: int
|
||||||
|
{
|
||||||
|
// Client
|
||||||
|
case BadRequest = 400;
|
||||||
|
case Unauthorized = 401;
|
||||||
|
case Forbidden = 403;
|
||||||
|
case NotFound = 404;
|
||||||
|
case MethodNotAllowed = 405;
|
||||||
|
case NotAcceptable = 406;
|
||||||
|
case ProxyAuthenticationRequired = 407;
|
||||||
|
case RequestTimeout = 408;
|
||||||
|
case Conflict = 409;
|
||||||
|
case Gone = 410;
|
||||||
|
case LengthRequired = 411;
|
||||||
|
case PreconditionFailed = 412;
|
||||||
|
case PayloadTooLarge = 413;
|
||||||
|
case UriTooLong = 414;
|
||||||
|
case UnsupportedMediaType = 415;
|
||||||
|
|
||||||
|
// Server
|
||||||
|
case InternalServerError = 500;
|
||||||
|
case NotImplemented = 501;
|
||||||
|
case BadGateway = 502;
|
||||||
|
case ServiceUnavailable = 503;
|
||||||
|
case GatewayTimeout = 504;
|
||||||
|
case HTTPVersionNotSupported = 505;
|
||||||
|
}
|
||||||
37
app/Enums/ServerState.php
Normal file
37
app/Enums/ServerState.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum ServerState: string
|
||||||
|
{
|
||||||
|
case Normal = 'normal';
|
||||||
|
case Installing = 'installing';
|
||||||
|
case InstallFailed = 'install_failed';
|
||||||
|
case ReinstallFailed = 'reinstall_failed';
|
||||||
|
case Suspended = 'suspended';
|
||||||
|
case RestoringBackup = 'restoring_backup';
|
||||||
|
|
||||||
|
public function icon(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Normal => 'tabler-heart',
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function color(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Normal => 'primary',
|
||||||
|
self::Installing => 'primary',
|
||||||
|
self::InstallFailed => 'danger',
|
||||||
|
self::ReinstallFailed => 'danger',
|
||||||
|
self::Suspended => 'warning',
|
||||||
|
self::RestoringBackup => 'primary',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,11 @@
|
|||||||
namespace App\Exceptions;
|
namespace App\Exceptions;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Prologue\Alerts\AlertsMessageBag;
|
use Prologue\Alerts\AlertsMessageBag;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||||
|
|
||||||
@@ -47,8 +46,18 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
|||||||
* and then redirecting them back to the page that they came from. If the
|
* and then redirecting them back to the page that they came from. If the
|
||||||
* request originated from an API hit, return the error in JSONAPI spec format.
|
* request originated from an API hit, return the error in JSONAPI spec format.
|
||||||
*/
|
*/
|
||||||
public function render(Request $request): JsonResponse|RedirectResponse
|
public function render(Request $request)
|
||||||
{
|
{
|
||||||
|
if ($request->is('livewire/update')) {
|
||||||
|
Notification::make()
|
||||||
|
->title(static::class)
|
||||||
|
->body($this->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->expectsJson()) {
|
if ($request->expectsJson()) {
|
||||||
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
|
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Exceptions\Http\Server;
|
namespace App\Exceptions\Http\Server;
|
||||||
|
|
||||||
|
use App\Enums\ServerState;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class ServerStateConflictException extends ConflictHttpException
|
|||||||
$message = 'The node of this server is currently under maintenance and the functionality requested is unavailable.';
|
$message = 'The node of this server is currently under maintenance and the functionality requested is unavailable.';
|
||||||
} elseif (!$server->isInstalled()) {
|
} elseif (!$server->isInstalled()) {
|
||||||
$message = 'This server has not yet completed its installation process, please try again later.';
|
$message = 'This server has not yet completed its installation process, please try again later.';
|
||||||
} elseif ($server->status === Server::STATUS_RESTORING_BACKUP) {
|
} elseif ($server->status === ServerState::RestoringBackup) {
|
||||||
$message = 'This server is currently restoring from a backup, please try again later.';
|
$message = 'This server is currently restoring from a backup, please try again later.';
|
||||||
} elseif (!is_null($server->transfer)) {
|
} elseif (!is_null($server->transfer)) {
|
||||||
$message = 'This server is currently being transferred to a new machine, please try again later.';
|
$message = 'This server is currently being transferred to a new machine, please try again later.';
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions\Service\Deployment;
|
|
||||||
|
|
||||||
use App\Exceptions\DisplayException;
|
|
||||||
|
|
||||||
class NoViableNodeException extends DisplayException
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -97,7 +97,7 @@ class BackupManager
|
|||||||
/**
|
/**
|
||||||
* Creates a new daemon adapter.
|
* Creates a new daemon adapter.
|
||||||
*/
|
*/
|
||||||
public function createDaemonAdapter(array $config): FilesystemAdapter
|
public function createWingsAdapter(array $config): FilesystemAdapter
|
||||||
{
|
{
|
||||||
return new InMemoryFilesystemAdapter();
|
return new InMemoryFilesystemAdapter();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class DynamicDatabaseConnection
|
|||||||
'port' => $host->port,
|
'port' => $host->port,
|
||||||
'database' => $database,
|
'database' => $database,
|
||||||
'username' => $host->username,
|
'username' => $host->username,
|
||||||
'password' => decrypt($host->password),
|
'password' => $host->password,
|
||||||
'charset' => self::DB_CHARSET,
|
'charset' => self::DB_CHARSET,
|
||||||
'collation' => self::DB_COLLATION,
|
'collation' => self::DB_COLLATION,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/* The MIT License (MIT)
|
||||||
|
|
||||||
|
Pterodactyl®
|
||||||
|
Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE. */
|
||||||
|
|
||||||
namespace App\Extensions\Filesystem;
|
namespace App\Extensions\Filesystem;
|
||||||
|
|
||||||
use Aws\S3\S3ClientInterface;
|
use Aws\S3\S3ClientInterface;
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Extensions;
|
|
||||||
|
|
||||||
use Hashids\Hashids as VendorHashids;
|
|
||||||
use App\Contracts\Extensions\HashidsInterface;
|
|
||||||
|
|
||||||
class Hashids extends VendorHashids implements HashidsInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function decodeFirst(string $encoded, string $default = null): mixed
|
|
||||||
{
|
|
||||||
$result = $this->decode($encoded);
|
|
||||||
if (!is_array($result)) {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_first($result, null, $default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
app/Filament/Clusters/Settings.php
Normal file
10
app/Filament/Clusters/Settings.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Clusters;
|
||||||
|
|
||||||
|
use Filament\Clusters\Cluster;
|
||||||
|
|
||||||
|
class Settings extends Cluster
|
||||||
|
{
|
||||||
|
protected static ?string $navigationIcon = 'tabler-settings';
|
||||||
|
}
|
||||||
67
app/Filament/Pages/Dashboard.php
Normal file
67
app/Filament/Pages/Dashboard.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\NodeResource\Pages\ListNodes;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use App\Models\Node;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
|
||||||
|
class Dashboard extends Page
|
||||||
|
{
|
||||||
|
protected static ?string $navigationIcon = 'tabler-layout-dashboard';
|
||||||
|
|
||||||
|
protected static string $view = 'filament.pages.dashboard';
|
||||||
|
|
||||||
|
protected ?string $heading = '';
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return trans('strings.dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static ?string $slug = '/';
|
||||||
|
|
||||||
|
public string $activeTab = 'nodes';
|
||||||
|
|
||||||
|
public function getViewData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'inDevelopment' => config('app.version') === 'canary',
|
||||||
|
'eggsCount' => Egg::query()->count(),
|
||||||
|
'nodesList' => ListNodes::getUrl(),
|
||||||
|
'nodesCount' => Node::query()->count(),
|
||||||
|
'serversCount' => Server::query()->count(),
|
||||||
|
'usersCount' => User::query()->count(),
|
||||||
|
|
||||||
|
'devActions' => [
|
||||||
|
CreateAction::make()
|
||||||
|
->label('Bugs & Features')
|
||||||
|
->icon('tabler-brand-github')
|
||||||
|
->url('https://github.com/pelican-dev/panel/discussions', true),
|
||||||
|
],
|
||||||
|
'nodeActions' => [
|
||||||
|
CreateAction::make()
|
||||||
|
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
|
||||||
|
->icon('tabler-server-2')
|
||||||
|
->url(route('filament.admin.resources.nodes.create')),
|
||||||
|
],
|
||||||
|
'supportActions' => [
|
||||||
|
CreateAction::make()
|
||||||
|
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||||
|
->icon('tabler-cash')
|
||||||
|
->url('https://pelican.dev/donate', true)
|
||||||
|
->color('success'),
|
||||||
|
],
|
||||||
|
'helpActions' => [
|
||||||
|
CreateAction::make()
|
||||||
|
->label(trans('dashboard/index.sections.intro-help.button_docs'))
|
||||||
|
->icon('tabler-speedboat')
|
||||||
|
->url('https://pelican.dev/docs', true),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/Filament/Resources/ApiKeyResource.php
Normal file
39
app/Filament/Resources/ApiKeyResource.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ApiKeyResource\Pages;
|
||||||
|
use App\Models\ApiKey;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
|
||||||
|
class ApiKeyResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = ApiKey::class;
|
||||||
|
protected static ?string $label = 'API Key';
|
||||||
|
protected static ?string $navigationIcon = 'tabler-key';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::where('key_type', '2')->count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canEdit($record): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListApiKeys::route('/'),
|
||||||
|
'create' => Pages\CreateApiKey::route('/create'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
88
app/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php
Normal file
88
app/Filament/Resources/ApiKeyResource/Pages/CreateApiKey.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ApiKeyResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ApiKeyResource;
|
||||||
|
use App\Models\ApiKey;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Filament\Forms;
|
||||||
|
|
||||||
|
class CreateApiKey extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = ApiKeyResource::class;
|
||||||
|
|
||||||
|
protected ?string $heading = 'Create Application API Key';
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Hidden::make('identifier')->default(ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION)),
|
||||||
|
Forms\Components\Hidden::make('token')->default(str_random(ApiKey::KEY_LENGTH)),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('user_id')
|
||||||
|
->default(auth()->user()->id)
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('key_type')
|
||||||
|
->inlineLabel()
|
||||||
|
->default(ApiKey::TYPE_APPLICATION)
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Fieldset::make('Permissions')
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 2,
|
||||||
|
])
|
||||||
|
->schema(
|
||||||
|
collect(ApiKey::RESOURCES)->map(fn ($resource) => Forms\Components\ToggleButtons::make("r_$resource")
|
||||||
|
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||||
|
->options([
|
||||||
|
0 => 'None',
|
||||||
|
1 => 'Read',
|
||||||
|
// 2 => 'Write',
|
||||||
|
3 => 'Read & Write',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
0 => 'tabler-book-off',
|
||||||
|
1 => 'tabler-book',
|
||||||
|
2 => 'tabler-writing',
|
||||||
|
3 => 'tabler-writing',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
0 => 'success',
|
||||||
|
1 => 'warning',
|
||||||
|
2 => 'danger',
|
||||||
|
3 => 'danger',
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
])
|
||||||
|
->default(0),
|
||||||
|
)->all(),
|
||||||
|
),
|
||||||
|
|
||||||
|
Forms\Components\TagsInput::make('allowed_ips')
|
||||||
|
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
||||||
|
->label('Whitelisted IPv4 Addresses')
|
||||||
|
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
|
||||||
|
->columnSpanFull()
|
||||||
|
->hidden()
|
||||||
|
->default(null),
|
||||||
|
|
||||||
|
Forms\Components\Textarea::make('memo')
|
||||||
|
->required()
|
||||||
|
->label('Description')
|
||||||
|
->helperText('
|
||||||
|
Once you have assigned permissions and created this set of credentials you will be unable to come back and edit it.
|
||||||
|
If you need to make changes down the road you will need to create a new set of credentials.
|
||||||
|
')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php
Normal file
62
app/Filament/Resources/ApiKeyResource/Pages/ListApiKeys.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ApiKeyResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ApiKeyResource;
|
||||||
|
use App\Models\ApiKey;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables;
|
||||||
|
|
||||||
|
class ListApiKeys extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = ApiKeyResource::class;
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->modifyQueryUsing(fn ($query) => $query->where('key_type', ApiKey::TYPE_APPLICATION))
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('key')
|
||||||
|
->copyable()
|
||||||
|
->icon('tabler-clipboard-text')
|
||||||
|
->state(fn (ApiKey $key) => $key->identifier . $key->token),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('memo')
|
||||||
|
->label('Description')
|
||||||
|
->wrap()
|
||||||
|
->limit(50),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('identifier')
|
||||||
|
->hidden()
|
||||||
|
->searchable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('last_used_at')
|
||||||
|
->label('Last Used')
|
||||||
|
->placeholder('Not Used')
|
||||||
|
->dateTime()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->label('Created')
|
||||||
|
->dateTime()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('user.username')
|
||||||
|
->label('Created By')
|
||||||
|
->url(fn (ApiKey $apiKey): string => route('filament.admin.resources.users.edit', ['record' => $apiKey->user])),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\DeleteAction::make(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Filament/Resources/DatabaseHostResource.php
Normal file
37
app/Filament/Resources/DatabaseHostResource.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\DatabaseHostResource\Pages;
|
||||||
|
use App\Models\DatabaseHost;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
|
||||||
|
class DatabaseHostResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = DatabaseHost::class;
|
||||||
|
|
||||||
|
protected static ?string $label = 'Databases';
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-database';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListDatabaseHosts::route('/'),
|
||||||
|
'create' => Pages\CreateDatabaseHost::route('/create'),
|
||||||
|
'edit' => Pages\EditDatabaseHost::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\DatabaseHostResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
|
||||||
|
class CreateDatabaseHost extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = DatabaseHostResource::class;
|
||||||
|
|
||||||
|
protected ?string $heading = 'Database Hosts';
|
||||||
|
|
||||||
|
protected static bool $canCreateAnother = false;
|
||||||
|
|
||||||
|
protected ?string $subheading = '(database servers that can have individual databases)';
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Section::make()
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 3,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 4,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('host')
|
||||||
|
->columnSpan(2)
|
||||||
|
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||||
|
->required()
|
||||||
|
->live(onBlur: true)
|
||||||
|
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('port')
|
||||||
|
->columnSpan(1)
|
||||||
|
->helperText('The port that MySQL is running on for this host.')
|
||||||
|
->required()
|
||||||
|
->numeric()
|
||||||
|
->default(3306)
|
||||||
|
->minValue(0)
|
||||||
|
->maxValue(65535),
|
||||||
|
Forms\Components\TextInput::make('max_databases')
|
||||||
|
->label('Max databases')
|
||||||
|
->helpertext('Blank is unlimited.')
|
||||||
|
->numeric(),
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->label('Display Name')
|
||||||
|
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||||
|
->required()
|
||||||
|
->maxLength(60),
|
||||||
|
Forms\Components\TextInput::make('username')
|
||||||
|
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('password')
|
||||||
|
->helperText('The password for the database user.')
|
||||||
|
->password()
|
||||||
|
->revealable()
|
||||||
|
->maxLength(191)
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Select::make('node_id')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||||
|
->label('Linked Node')
|
||||||
|
->relationship('node', 'name'),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$this->getCreateFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\DatabaseHostResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
|
||||||
|
class EditDatabaseHost extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = DatabaseHostResource::class;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Section::make()
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 3,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 4,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('host')
|
||||||
|
->columnSpan(2)
|
||||||
|
->helperText('The IP address or Domain name that should be used when attempting to connect to this MySQL host from this Panel to create new databases.')
|
||||||
|
->required()
|
||||||
|
->live(onBlur: true)
|
||||||
|
->afterStateUpdated(fn ($state, Forms\Set $set) => $set('name', $state))
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('port')
|
||||||
|
->columnSpan(1)
|
||||||
|
->helperText('The port that MySQL is running on for this host.')
|
||||||
|
->required()
|
||||||
|
->numeric()
|
||||||
|
->minValue(0)
|
||||||
|
->maxValue(65535),
|
||||||
|
Forms\Components\TextInput::make('max_databases')
|
||||||
|
->label('Max databases')
|
||||||
|
->helpertext('Blank is unlimited.')
|
||||||
|
->numeric(),
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->label('Display Name')
|
||||||
|
->helperText('A short identifier used to distinguish this location from others. Must be between 1 and 60 characters, for example, us.nyc.lvl3.')
|
||||||
|
->required()
|
||||||
|
->maxLength(60),
|
||||||
|
Forms\Components\TextInput::make('username')
|
||||||
|
->helperText('The username of an account that has enough permissions to create new users and databases on the system.')
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('password')
|
||||||
|
->helperText('The password for the database user.')
|
||||||
|
->password()
|
||||||
|
->revealable()
|
||||||
|
->maxLength(191)
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Select::make('node_id')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||||
|
->label('Linked Node')
|
||||||
|
->relationship('node', 'name'),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelationManagers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DatabaseHostResource\RelationManagers\DatabasesRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\DatabaseHostResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class ListDatabaseHosts extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = DatabaseHostResource::class;
|
||||||
|
|
||||||
|
protected ?string $heading = 'Database Hosts';
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('host')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('port')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('username')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('max_databases')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('node.name')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make('create')->label('New Database Host'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
|
||||||
|
|
||||||
|
use App\Models\Database;
|
||||||
|
use App\Services\Databases\DatabasePasswordService;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Actions\Action;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class DatabasesRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'databases';
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('database')->columnSpanFull(),
|
||||||
|
Forms\Components\TextInput::make('username'),
|
||||||
|
Forms\Components\TextInput::make('password')
|
||||||
|
->hintAction(
|
||||||
|
Action::make('rotate')
|
||||||
|
->icon('tabler-refresh')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
|
||||||
|
)
|
||||||
|
->formatStateUsing(fn (Database $database) => $database->password),
|
||||||
|
Forms\Components\TextInput::make('remote')->label('Connections From'),
|
||||||
|
Forms\Components\TextInput::make('max_connections'),
|
||||||
|
Forms\Components\TextInput::make('JDBC')
|
||||||
|
->label('JDBC Connection String')
|
||||||
|
->columnSpanFull()
|
||||||
|
->formatStateUsing(fn (Forms\Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->recordTitleAttribute('servers')
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('database')->icon('tabler-database'),
|
||||||
|
Tables\Columns\TextColumn::make('username')->icon('tabler-user'),
|
||||||
|
//Tables\Columns\TextColumn::make('password'),
|
||||||
|
Tables\Columns\TextColumn::make('remote'),
|
||||||
|
Tables\Columns\TextColumn::make('server.name')
|
||||||
|
->icon('tabler-brand-docker')
|
||||||
|
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
|
||||||
|
Tables\Columns\TextColumn::make('max_connections'),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')->dateTime(),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\DeleteAction::make(),
|
||||||
|
Tables\Actions\ViewAction::make()->color('primary'),
|
||||||
|
//Tables\Actions\EditAction::make(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void
|
||||||
|
{
|
||||||
|
$newPassword = $service->handle($database);
|
||||||
|
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database');
|
||||||
|
|
||||||
|
$set('password', $newPassword);
|
||||||
|
$set('JDBC', $jdbcString);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Filament/Resources/DatabaseResource.php
Normal file
37
app/Filament/Resources/DatabaseResource.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\DatabaseResource\Pages;
|
||||||
|
use App\Models\Database;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
|
||||||
|
class DatabaseResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Database::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-database';
|
||||||
|
|
||||||
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListDatabases::route('/'),
|
||||||
|
'create' => Pages\CreateDatabase::route('/create'),
|
||||||
|
'edit' => Pages\EditDatabase::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\DatabaseResource;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Filament\Forms;
|
||||||
|
|
||||||
|
class CreateDatabase extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = DatabaseResource::class;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('server_id')
|
||||||
|
->relationship('server', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('database_host_id')
|
||||||
|
->required()
|
||||||
|
->numeric(),
|
||||||
|
Forms\Components\TextInput::make('database')
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('remote')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->default('%'),
|
||||||
|
Forms\Components\TextInput::make('username')
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('password')
|
||||||
|
->password()
|
||||||
|
->revealable()
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('max_connections')
|
||||||
|
->numeric()
|
||||||
|
->minValue(0)
|
||||||
|
->default(0),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\DatabaseResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Filament\Forms;
|
||||||
|
|
||||||
|
class EditDatabase extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = DatabaseResource::class;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('server_id')
|
||||||
|
->relationship('server', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('database_host_id')
|
||||||
|
->required()
|
||||||
|
->numeric(),
|
||||||
|
Forms\Components\TextInput::make('database')
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('remote')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->default('%'),
|
||||||
|
Forms\Components\TextInput::make('username')
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('password')
|
||||||
|
->password()
|
||||||
|
->revealable()
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('max_connections')
|
||||||
|
->numeric()
|
||||||
|
->minValue(0)
|
||||||
|
->default(0),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\DatabaseResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables;
|
||||||
|
|
||||||
|
class ListDatabases extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = DatabaseResource::class;
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('server.name')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('database_host_id')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('database')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('username')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('remote')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('max_connections')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
Tables\Columns\TextColumn::make('updated_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Filament/Resources/EggResource.php
Normal file
44
app/Filament/Resources/EggResource.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\EggResource\Pages;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
|
||||||
|
class EggResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Egg::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-eggs';
|
||||||
|
|
||||||
|
protected static ?string $recordTitleAttribute = 'name';
|
||||||
|
|
||||||
|
protected static ?string $recordRouteKeyName = 'id';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getGloballySearchableAttributes(): array
|
||||||
|
{
|
||||||
|
return ['name', 'tags', 'uuid', 'id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListEggs::route('/'),
|
||||||
|
'create' => Pages\CreateEgg::route('/create'),
|
||||||
|
'edit' => Pages\EditEgg::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
213
app/Filament/Resources/EggResource/Pages/CreateEgg.php
Normal file
213
app/Filament/Resources/EggResource/Pages/CreateEgg.php
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\EggResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\EggResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class CreateEgg extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = EggResource::class;
|
||||||
|
|
||||||
|
protected static bool $canCreateAnother = false;
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Tabs::make()->tabs([
|
||||||
|
Forms\Components\Tabs\Tab::make('Configuration')
|
||||||
|
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||||
|
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||||
|
Forms\Components\TextInput::make('author')
|
||||||
|
->maxLength(191)
|
||||||
|
->required()
|
||||||
|
->email()
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||||
|
->helperText('The author of this version of the Egg.'),
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->rows(3)
|
||||||
|
->columnSpanFull()
|
||||||
|
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
|
||||||
|
Forms\Components\Textarea::make('startup')
|
||||||
|
->rows(3)
|
||||||
|
->columnSpanFull()
|
||||||
|
->required()
|
||||||
|
->placeholder(implode("\n", [
|
||||||
|
'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}',
|
||||||
|
]))
|
||||||
|
->helperText('The default startup command that should be used for new servers using this Egg.'),
|
||||||
|
Forms\Components\TagsInput::make('features')
|
||||||
|
->placeholder('Add Feature')
|
||||||
|
->helperText('')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||||
|
Forms\Components\Toggle::make('force_outgoing_ip')
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||||
|
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||||
|
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||||
|
Forms\Components\Hidden::make('script_is_privileged')
|
||||||
|
->default(1),
|
||||||
|
Forms\Components\TagsInput::make('tags')
|
||||||
|
->placeholder('Add Tags')
|
||||||
|
->helperText('')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||||
|
Forms\Components\TextInput::make('update_url')
|
||||||
|
->disabled()
|
||||||
|
->helperText('Not implemented.')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||||
|
Forms\Components\KeyValue::make('docker_images')
|
||||||
|
->live()
|
||||||
|
->columnSpanFull()
|
||||||
|
->required()
|
||||||
|
->addActionLabel('Add Image')
|
||||||
|
->keyLabel('Name')
|
||||||
|
->keyPlaceholder('Java 21')
|
||||||
|
->valueLabel('Image URI')
|
||||||
|
->valuePlaceholder('ghcr.io/parkervcp/yolks:java_21')
|
||||||
|
->helperText('The docker images available to servers using this egg.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Tabs\Tab::make('Process Management')
|
||||||
|
->columns()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Hidden::make('config_from')
|
||||||
|
->default(null)
|
||||||
|
->label('Copy Settings From')
|
||||||
|
// ->placeholder('None')
|
||||||
|
// ->relationship('configFrom', 'name', ignoreRecord: true)
|
||||||
|
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
|
||||||
|
Forms\Components\TextInput::make('config_stop')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->label('Stop Command')
|
||||||
|
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
|
||||||
|
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
|
||||||
|
->label('Start Configuration')
|
||||||
|
->default('{}')
|
||||||
|
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
|
||||||
|
Forms\Components\Textarea::make('config_files')->rows(10)->json()
|
||||||
|
->label('Configuration Files')
|
||||||
|
->default('{}')
|
||||||
|
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
|
||||||
|
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
|
||||||
|
->label('Log Configuration')
|
||||||
|
->default('{}')
|
||||||
|
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Tabs\Tab::make('Egg Variables')
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Repeater::make('variables')
|
||||||
|
->label('')
|
||||||
|
->addActionLabel('Add New Egg Variable')
|
||||||
|
->grid()
|
||||||
|
->relationship('variables')
|
||||||
|
->name('name')
|
||||||
|
->reorderable()->orderColumn()
|
||||||
|
->collapsible()->collapsed()
|
||||||
|
->columnSpan(2)
|
||||||
|
->defaultItems(0)
|
||||||
|
->itemLabel(fn (array $state) => $state['name'])
|
||||||
|
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||||
|
$data['default_value'] ??= '';
|
||||||
|
$data['description'] ??= '';
|
||||||
|
$data['rules'] ??= '';
|
||||||
|
$data['user_viewable'] ??= '';
|
||||||
|
$data['user_editable'] ??= '';
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
})
|
||||||
|
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||||
|
$data['default_value'] ??= '';
|
||||||
|
$data['description'] ??= '';
|
||||||
|
$data['rules'] ??= '';
|
||||||
|
$data['user_viewable'] ??= '';
|
||||||
|
$data['user_editable'] ??= '';
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
})
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->live()
|
||||||
|
->debounce(750)
|
||||||
|
->maxLength(191)
|
||||||
|
->columnSpanFull()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||||
|
)
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Textarea::make('description')->columnSpanFull(),
|
||||||
|
Forms\Components\TextInput::make('env_variable')
|
||||||
|
->label('Environment Variable')
|
||||||
|
->maxLength(191)
|
||||||
|
->prefix('{{')
|
||||||
|
->suffix('}}')
|
||||||
|
->hintIcon('tabler-code')
|
||||||
|
->hintIconTooltip(fn ($state) => "{{{$state}}}")
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('default_value')->maxLength(191),
|
||||||
|
Forms\Components\Fieldset::make('User Permissions')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Checkbox::make('user_viewable')->label('Viewable'),
|
||||||
|
Forms\Components\Checkbox::make('user_editable')->label('Editable'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Textarea::make('rules')->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Forms\Components\Tabs\Tab::make('Install Script')
|
||||||
|
->columns(3)
|
||||||
|
->schema([
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('copy_script_from'),
|
||||||
|
//->placeholder('None')
|
||||||
|
//->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('script_container')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->default('alpine:3.4'),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('script_entry')
|
||||||
|
->selectablePlaceholder(false)
|
||||||
|
->default('bash')
|
||||||
|
->options(['bash', 'ash', '/bin/bash'])
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
MonacoEditor::make('script_install')
|
||||||
|
->columnSpanFull()
|
||||||
|
->fontSize('16px')
|
||||||
|
->language('shell')
|
||||||
|
->lazy()
|
||||||
|
->view('filament.plugins.monaco-editor'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
])->columnSpanFull()->persistTabInQueryString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function handleRecordCreation(array $data): Model
|
||||||
|
{
|
||||||
|
$data['uuid'] ??= Str::uuid()->toString();
|
||||||
|
|
||||||
|
if (is_array($data['config_startup'])) {
|
||||||
|
$data['config_startup'] = json_encode($data['config_startup']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($data['config_logs'])) {
|
||||||
|
$data['config_logs'] = json_encode($data['config_logs']);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger()->info('new egg', $data);
|
||||||
|
|
||||||
|
return parent::handleRecordCreation($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
225
app/Filament/Resources/EggResource/Pages/EditEgg.php
Normal file
225
app/Filament/Resources/EggResource/Pages/EditEgg.php
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\EggResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\EggResource;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
|
||||||
|
class EditEgg extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = EggResource::class;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Tabs::make()->tabs([
|
||||||
|
Forms\Components\Tabs\Tab::make('Configuration')
|
||||||
|
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||||
|
->helperText('A simple, human-readable name to use as an identifier for this Egg.'),
|
||||||
|
Forms\Components\TextInput::make('uuid')
|
||||||
|
->label('Egg UUID')
|
||||||
|
->disabled()
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||||
|
->helperText('This is the globally unique identifier for this Egg which Wings uses as an identifier.'),
|
||||||
|
Forms\Components\TextInput::make('id')
|
||||||
|
->label('Egg ID')
|
||||||
|
->disabled(),
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->rows(3)
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||||
|
->helperText('A description of this Egg that will be displayed throughout the Panel as needed.'),
|
||||||
|
Forms\Components\TextInput::make('author')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->email()
|
||||||
|
->disabled()
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||||
|
->helperText('The author of this version of the Egg. Uploading a new Egg configuration from a different author will change this.'),
|
||||||
|
Forms\Components\Textarea::make('startup')
|
||||||
|
->rows(2)
|
||||||
|
->columnSpanFull()
|
||||||
|
->required()
|
||||||
|
->helperText('The default startup command that should be used for new servers using this Egg.'),
|
||||||
|
Forms\Components\TagsInput::make('file_denylist')
|
||||||
|
->hidden() // latest wings breaks it.
|
||||||
|
->placeholder('denied-file.txt')
|
||||||
|
->helperText('A list of files that the end user is not allowed to edit.')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||||
|
Forms\Components\TagsInput::make('features')
|
||||||
|
->placeholder('Add Feature')
|
||||||
|
->helperText('')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||||
|
Forms\Components\Toggle::make('force_outgoing_ip')
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||||
|
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||||
|
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||||
|
Forms\Components\Hidden::make('script_is_privileged')
|
||||||
|
->helperText('The docker images available to servers using this egg.'),
|
||||||
|
Forms\Components\TagsInput::make('tags')
|
||||||
|
->placeholder('Add Tags')
|
||||||
|
->helperText('')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||||
|
Forms\Components\TextInput::make('update_url')
|
||||||
|
->disabled()
|
||||||
|
->helperText('Not implemented.')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||||
|
Forms\Components\KeyValue::make('docker_images')
|
||||||
|
->live()
|
||||||
|
->columnSpanFull()
|
||||||
|
->required()
|
||||||
|
->addActionLabel('Add Image')
|
||||||
|
->keyLabel('Name')
|
||||||
|
->valueLabel('Image URI')
|
||||||
|
->helperText('The docker images available to servers using this egg.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Tabs\Tab::make('Process Management')
|
||||||
|
->columns()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('config_from')
|
||||||
|
->label('Copy Settings From')
|
||||||
|
->placeholder('None')
|
||||||
|
->relationship('configFrom', 'name', ignoreRecord: true)
|
||||||
|
->helperText('If you would like to default to settings from another Egg select it from the menu above.'),
|
||||||
|
Forms\Components\TextInput::make('config_stop')
|
||||||
|
->maxLength(191)
|
||||||
|
->label('Stop Command')
|
||||||
|
->helperText('The command that should be sent to server processes to stop them gracefully. If you need to send a SIGINT you should enter ^C here.'),
|
||||||
|
Forms\Components\Textarea::make('config_startup')->rows(10)->json()
|
||||||
|
->label('Start Configuration')
|
||||||
|
->helperText('List of values the daemon should be looking for when booting a server to determine completion.'),
|
||||||
|
Forms\Components\Textarea::make('config_files')->rows(10)->json()
|
||||||
|
->label('Configuration Files')
|
||||||
|
->helperText('This should be a JSON representation of configuration files to modify and what parts should be changed.'),
|
||||||
|
Forms\Components\Textarea::make('config_logs')->rows(10)->json()
|
||||||
|
->label('Log Configuration')
|
||||||
|
->helperText('This should be a JSON representation of where log files are stored, and whether or not the daemon should be creating custom logs.'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Tabs\Tab::make('Egg Variables')
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Repeater::make('variables')
|
||||||
|
->label('')
|
||||||
|
->grid()
|
||||||
|
->relationship('variables')
|
||||||
|
->name('name')
|
||||||
|
->reorderable()
|
||||||
|
->collapsible()->collapsed()
|
||||||
|
->orderColumn()
|
||||||
|
->addActionLabel('New Variable')
|
||||||
|
->itemLabel(fn (array $state) => $state['name'])
|
||||||
|
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||||
|
$data['default_value'] ??= '';
|
||||||
|
$data['description'] ??= '';
|
||||||
|
$data['rules'] ??= '';
|
||||||
|
$data['user_viewable'] ??= '';
|
||||||
|
$data['user_editable'] ??= '';
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
})
|
||||||
|
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||||
|
$data['default_value'] ??= '';
|
||||||
|
$data['description'] ??= '';
|
||||||
|
$data['rules'] ??= '';
|
||||||
|
$data['user_viewable'] ??= '';
|
||||||
|
$data['user_editable'] ??= '';
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
})
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->live()
|
||||||
|
->debounce(750)
|
||||||
|
->maxLength(191)
|
||||||
|
->columnSpanFull()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||||
|
)
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Textarea::make('description')->columnSpanFull(),
|
||||||
|
Forms\Components\TextInput::make('env_variable')
|
||||||
|
->label('Environment Variable')
|
||||||
|
->maxLength(191)
|
||||||
|
->prefix('{{')
|
||||||
|
->suffix('}}')
|
||||||
|
->hintIcon('tabler-code')
|
||||||
|
->hintIconTooltip(fn ($state) => "{{{$state}}}")
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('default_value')->maxLength(191),
|
||||||
|
Forms\Components\Fieldset::make('User Permissions')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Checkbox::make('user_viewable')->label('Viewable'),
|
||||||
|
Forms\Components\Checkbox::make('user_editable')->label('Editable'),
|
||||||
|
]),
|
||||||
|
Forms\Components\TextInput::make('rules')->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Forms\Components\Tabs\Tab::make('Install Script')
|
||||||
|
->columns(3)
|
||||||
|
->schema([
|
||||||
|
|
||||||
|
Forms\Components\Select::make('copy_script_from')
|
||||||
|
->placeholder('None')
|
||||||
|
->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('script_container')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->default('alpine:3.4'),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('script_entry')
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->default('ash'),
|
||||||
|
|
||||||
|
MonacoEditor::make('script_install')
|
||||||
|
->label('Install Script')
|
||||||
|
->columnSpanFull()
|
||||||
|
->fontSize('16px')
|
||||||
|
->language('shell')
|
||||||
|
->view('filament.plugins.monaco-editor'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
])->columnSpanFull()->persistTabInQueryString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make()
|
||||||
|
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||||
|
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete Egg' : 'Egg In Use'),
|
||||||
|
Actions\ExportAction::make()
|
||||||
|
->icon('tabler-download')
|
||||||
|
->label('Export Egg')
|
||||||
|
->color('primary')
|
||||||
|
// TODO uses old admin panel export service
|
||||||
|
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg['id']])),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelationManagers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
EggResource\RelationManagers\ServersRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
99
app/Filament/Resources/EggResource/Pages/ListEggs.php
Normal file
99
app/Filament/Resources/EggResource/Pages/ListEggs.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\EggResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\EggResource;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use App\Services\Eggs\Sharing\EggImporterService;
|
||||||
|
use Exception;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
|
use Filament\Tables;
|
||||||
|
|
||||||
|
class ListEggs extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = EggResource::class;
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->defaultPaginationPageOption(25)
|
||||||
|
->checkIfRecordIsSelectableUsing(fn (Egg $egg) => $egg->servers_count <= 0)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('id')
|
||||||
|
->label('Id')
|
||||||
|
->hidden()
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->icon('tabler-egg')
|
||||||
|
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
|
||||||
|
->wrap()
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('servers_count')
|
||||||
|
->counts('servers')
|
||||||
|
->icon('tabler-server')
|
||||||
|
->label('Servers'),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\ExportAction::make()
|
||||||
|
->icon('tabler-download')
|
||||||
|
->label('Export')
|
||||||
|
->color('primary')
|
||||||
|
// TODO uses old admin panel export service
|
||||||
|
->url(fn (Egg $egg): string => route('admin.eggs.export', ['egg' => $egg])),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make('create')->label('Create Egg'),
|
||||||
|
|
||||||
|
Actions\Action::make('import')
|
||||||
|
->label('Import')
|
||||||
|
->form([
|
||||||
|
Forms\Components\FileUpload::make('egg')
|
||||||
|
->acceptedFileTypes(['application/json'])
|
||||||
|
->storeFiles(false)
|
||||||
|
->multiple(),
|
||||||
|
])
|
||||||
|
->action(function (array $data): void {
|
||||||
|
/** @var TemporaryUploadedFile $eggFile */
|
||||||
|
$eggFile = $data['egg'];
|
||||||
|
|
||||||
|
/** @var EggImporterService $eggImportService */
|
||||||
|
$eggImportService = resolve(EggImporterService::class);
|
||||||
|
|
||||||
|
foreach ($eggFile as $file) {
|
||||||
|
try {
|
||||||
|
$eggImportService->handle($file);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Import Failed')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
report($exception);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Import Success')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\EggResource\RelationManagers;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class ServersRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'servers';
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->recordTitleAttribute('servers')
|
||||||
|
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
|
||||||
|
->searchable(false)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('user.username')
|
||||||
|
->label('Owner')
|
||||||
|
->icon('tabler-user')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->icon('tabler-brand-docker')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('node.name')
|
||||||
|
->icon('tabler-server-2')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
|
||||||
|
Tables\Columns\TextColumn::make('image')
|
||||||
|
->label('Docker Image'),
|
||||||
|
Tables\Columns\SelectColumn::make('allocation.id')
|
||||||
|
->label('Primary Allocation')
|
||||||
|
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||||
|
->selectablePlaceholder(false)
|
||||||
|
->sortable(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Filament/Resources/MountResource.php
Normal file
35
app/Filament/Resources/MountResource.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\MountResource\Pages;
|
||||||
|
use App\Models\Mount;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
|
||||||
|
class MountResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Mount::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListMounts::route('/'),
|
||||||
|
'create' => Pages\CreateMount::route('/create'),
|
||||||
|
'edit' => Pages\EditMount::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
108
app/Filament/Resources/MountResource/Pages/CreateMount.php
Normal file
108
app/Filament/Resources/MountResource/Pages/CreateMount.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\MountResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\MountResource;
|
||||||
|
use Filament\Forms\Components\Group;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class CreateMount extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = MountResource::class;
|
||||||
|
|
||||||
|
protected static bool $canCreateAnother = false;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Section::make()->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->required()
|
||||||
|
->helperText('Unique name used to separate this mount from another.')
|
||||||
|
->maxLength(64),
|
||||||
|
Forms\Components\ToggleButtons::make('read_only')
|
||||||
|
->label('Read only?')
|
||||||
|
->helperText('Is the mount read only inside the container?')
|
||||||
|
->options([
|
||||||
|
false => 'Writeable',
|
||||||
|
true => 'Read only',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
false => 'tabler-writing',
|
||||||
|
true => 'tabler-writing-off',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'warning',
|
||||||
|
true => 'success',
|
||||||
|
])
|
||||||
|
->inline()
|
||||||
|
->default(false)
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('source')
|
||||||
|
->required()
|
||||||
|
->helperText('File path on the host system to mount to a container.')
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('target')
|
||||||
|
->required()
|
||||||
|
->helperText('Where the mount will be accessible inside a container.')
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\ToggleButtons::make('user_mountable')
|
||||||
|
->hidden()
|
||||||
|
->label('User mountable?')
|
||||||
|
->options([
|
||||||
|
false => 'No',
|
||||||
|
true => 'Yes',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
false => 'tabler-user-cancel',
|
||||||
|
true => 'tabler-user-bolt',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'success',
|
||||||
|
true => 'warning',
|
||||||
|
])
|
||||||
|
->default(false)
|
||||||
|
->inline()
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->helperText('A longer description for this mount.')
|
||||||
|
->columnSpanFull(),
|
||||||
|
Forms\Components\Hidden::make('user_mountable')->default(1),
|
||||||
|
])->columnSpan(1)->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
]),
|
||||||
|
Group::make()->schema([
|
||||||
|
Section::make()->schema([
|
||||||
|
Select::make('eggs')->multiple()
|
||||||
|
->relationship('eggs', 'name')
|
||||||
|
->preload(),
|
||||||
|
Select::make('nodes')->multiple()
|
||||||
|
->relationship('nodes', 'name')
|
||||||
|
->searchable(['name', 'fqdn'])
|
||||||
|
->preload(),
|
||||||
|
]),
|
||||||
|
])->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
]),
|
||||||
|
])->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function handleRecordCreation(array $data): Model
|
||||||
|
{
|
||||||
|
$data['uuid'] ??= Str::uuid()->toString();
|
||||||
|
|
||||||
|
return parent::handleRecordCreation($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
app/Filament/Resources/MountResource/Pages/EditMount.php
Normal file
109
app/Filament/Resources/MountResource/Pages/EditMount.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\MountResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\MountResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Group;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
|
||||||
|
class EditMount extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = MountResource::class;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Section::make()->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->required()
|
||||||
|
->helperText('Unique name used to separate this mount from another.')
|
||||||
|
->maxLength(64),
|
||||||
|
Forms\Components\ToggleButtons::make('read_only')
|
||||||
|
->label('Read only?')
|
||||||
|
->helperText('Is the mount read only inside the container?')
|
||||||
|
->options([
|
||||||
|
false => 'Writeable',
|
||||||
|
true => 'Read only',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
false => 'tabler-writing',
|
||||||
|
true => 'tabler-writing-off',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'warning',
|
||||||
|
true => 'success',
|
||||||
|
])
|
||||||
|
->inline()
|
||||||
|
->default(false)
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('source')
|
||||||
|
->required()
|
||||||
|
->helperText('File path on the host system to mount to a container.')
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('target')
|
||||||
|
->required()
|
||||||
|
->helperText('Where the mount will be accessible inside a container.')
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\ToggleButtons::make('user_mountable')
|
||||||
|
->hidden()
|
||||||
|
->label('User mountable?')
|
||||||
|
->options([
|
||||||
|
false => 'No',
|
||||||
|
true => 'Yes',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
false => 'tabler-user-cancel',
|
||||||
|
true => 'tabler-user-bolt',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'success',
|
||||||
|
true => 'warning',
|
||||||
|
])
|
||||||
|
->default(false)
|
||||||
|
->inline()
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->helperText('A longer description for this mount.')
|
||||||
|
->columnSpanFull(),
|
||||||
|
])->columnSpan(1)->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
]),
|
||||||
|
Group::make()->schema([
|
||||||
|
Section::make()->schema([
|
||||||
|
Select::make('eggs')->multiple()
|
||||||
|
->relationship('eggs', 'name')
|
||||||
|
->preload(),
|
||||||
|
Select::make('nodes')->multiple()
|
||||||
|
->relationship('nodes', 'name')
|
||||||
|
->searchable(['name', 'fqdn'])
|
||||||
|
->preload(),
|
||||||
|
]),
|
||||||
|
])->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
]),
|
||||||
|
])->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Filament/Resources/MountResource/Pages/ListMounts.php
Normal file
65
app/Filament/Resources/MountResource/Pages/ListMounts.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\MountResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\MountResource;
|
||||||
|
use App\Models\Mount;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Actions\CreateAction;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables;
|
||||||
|
|
||||||
|
class ListMounts extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = MountResource::class;
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('source')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('target')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\IconColumn::make('read_only')
|
||||||
|
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
|
||||||
|
->color(fn (bool $state) => $state ? 'success' : 'danger')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\IconColumn::make('user_mountable')
|
||||||
|
->hidden()
|
||||||
|
->icon(fn (bool $state) => $state ? 'tabler-circle-check-filled' : 'tabler-circle-x-filled')
|
||||||
|
->color(fn (bool $state) => $state ? 'success' : 'danger')
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->emptyStateIcon('tabler-layers-linked')
|
||||||
|
->emptyStateDescription('')
|
||||||
|
->emptyStateHeading('No Mounts')
|
||||||
|
->emptyStateActions([
|
||||||
|
CreateAction::make('create')
|
||||||
|
->label('Create Mount')
|
||||||
|
->button(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make()
|
||||||
|
->label('Create Mount')
|
||||||
|
->hidden(fn () => Mount::count() <= 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/Filament/Resources/NodeResource.php
Normal file
39
app/Filament/Resources/NodeResource.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\NodeResource\Pages;
|
||||||
|
use App\Filament\Resources\NodeResource\RelationManagers;
|
||||||
|
use App\Models\Node;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
|
||||||
|
class NodeResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Node::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-server-2';
|
||||||
|
|
||||||
|
protected static ?string $recordTitleAttribute = 'name';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
RelationManagers\AllocationsRelationManager::class,
|
||||||
|
RelationManagers\NodesRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListNodes::route('/'),
|
||||||
|
'create' => Pages\CreateNode::route('/create'),
|
||||||
|
'edit' => Pages\EditNode::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
366
app/Filament/Resources/NodeResource/Pages/CreateNode.php
Normal file
366
app/Filament/Resources/NodeResource/Pages/CreateNode.php
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\NodeResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\NodeResource;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Tabs;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
|
||||||
|
class CreateNode extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = NodeResource::class;
|
||||||
|
|
||||||
|
protected static bool $canCreateAnother = false;
|
||||||
|
|
||||||
|
protected ?string $subheading = 'which is a machine that runs your Servers';
|
||||||
|
|
||||||
|
public function form(Forms\Form $form): Forms\Form
|
||||||
|
{
|
||||||
|
return $form->schema([
|
||||||
|
Tabs::make('Tabs')
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 3,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 4,
|
||||||
|
])
|
||||||
|
->persistTabInQueryString()
|
||||||
|
->columnSpanFull()
|
||||||
|
->tabs([
|
||||||
|
Tabs\Tab::make('Basic Settings')
|
||||||
|
->icon('tabler-server')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('fqdn')
|
||||||
|
->columnSpan(2)
|
||||||
|
->required()
|
||||||
|
->autofocus()
|
||||||
|
->live(debounce: 1500)
|
||||||
|
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||||
|
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||||
|
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||||
|
->helperText(function ($state) {
|
||||||
|
if (is_ip($state)) {
|
||||||
|
if (request()->isSecure()) {
|
||||||
|
return '
|
||||||
|
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
|
||||||
|
You must use a domain name, because you cannot get SSL certificates for IP Addresses
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return "
|
||||||
|
This is the domain name that points to your node's IP Address.
|
||||||
|
If you've already set up this, you can verify it by checking the next field!
|
||||||
|
";
|
||||||
|
})
|
||||||
|
->hintColor('danger')
|
||||||
|
->hint(function ($state) {
|
||||||
|
if (is_ip($state) && request()->isSecure()) {
|
||||||
|
return 'You cannot connect to an IP Address over SSL';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||||
|
$set('dns', null);
|
||||||
|
$set('ip', null);
|
||||||
|
|
||||||
|
[$subdomain] = str($state)->explode('.', 2);
|
||||||
|
if (!is_numeric($subdomain)) {
|
||||||
|
$set('name', $subdomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$state || is_ip($state)) {
|
||||||
|
$set('dns', null);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validRecords = gethostbynamel($state);
|
||||||
|
if ($validRecords) {
|
||||||
|
$set('dns', true);
|
||||||
|
|
||||||
|
$set('ip', collect($validRecords)->first());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$set('dns', false);
|
||||||
|
})
|
||||||
|
->maxLength(191),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('ip')
|
||||||
|
->disabled()
|
||||||
|
->hidden(),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('dns')
|
||||||
|
->label('DNS Record Check')
|
||||||
|
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||||
|
->disabled()
|
||||||
|
->inline()
|
||||||
|
->default(null)
|
||||||
|
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||||
|
->hintColor('success')
|
||||||
|
->options([
|
||||||
|
true => 'Valid',
|
||||||
|
false => 'Invalid',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'success',
|
||||||
|
false => 'danger',
|
||||||
|
])
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 1,
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('daemon_listen')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 1,
|
||||||
|
])
|
||||||
|
->label(trans('strings.port'))
|
||||||
|
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||||
|
->minValue(0)
|
||||||
|
->maxValue(65536)
|
||||||
|
->default(8080)
|
||||||
|
->required()
|
||||||
|
->integer(),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->label('Display Name')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||||
|
->helperText('This name is for display only and can be changed later.')
|
||||||
|
->maxLength(100),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('scheme')
|
||||||
|
->label('Communicate over SSL')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 1,
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->inline()
|
||||||
|
->helperText(function (Forms\Get $get) {
|
||||||
|
if (request()->isSecure()) {
|
||||||
|
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_ip($get('fqdn'))) {
|
||||||
|
return 'An IP address cannot use SSL.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||||
|
->options([
|
||||||
|
'http' => 'HTTP',
|
||||||
|
'https' => 'HTTPS (SSL)',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
'http' => 'warning',
|
||||||
|
'https' => 'success',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
'http' => 'tabler-lock-open-off',
|
||||||
|
'https' => 'tabler-lock',
|
||||||
|
])
|
||||||
|
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||||
|
]),
|
||||||
|
Tabs\Tab::make('Advanced Settings')
|
||||||
|
->icon('tabler-server-cog')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('upload_size')
|
||||||
|
->label('Upload Limit')
|
||||||
|
->helperText('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||||
|
->columnSpan(1)
|
||||||
|
->numeric()->required()
|
||||||
|
->default(256)
|
||||||
|
->minValue(1)
|
||||||
|
->maxValue(1024)
|
||||||
|
->suffix('MiB'),
|
||||||
|
Forms\Components\ToggleButtons::make('public')
|
||||||
|
->label('Automatic Allocation')->inline()
|
||||||
|
->default(true)
|
||||||
|
->columnSpan(1)
|
||||||
|
->options([
|
||||||
|
true => 'Yes',
|
||||||
|
false => 'No',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'success',
|
||||||
|
false => 'danger',
|
||||||
|
]),
|
||||||
|
Forms\Components\ToggleButtons::make('maintenance_mode')
|
||||||
|
->label('Maintenance Mode')->inline()
|
||||||
|
->columnSpan(1)
|
||||||
|
->default(false)
|
||||||
|
->hinticon('tabler-question-mark')
|
||||||
|
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||||
|
->options([
|
||||||
|
true => 'Enable',
|
||||||
|
false => 'Disable',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'danger',
|
||||||
|
false => 'success',
|
||||||
|
]),
|
||||||
|
Forms\Components\TagsInput::make('tags')
|
||||||
|
->label('Tags')
|
||||||
|
->disabled()
|
||||||
|
->placeholder('Not Implemented')
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('Not Implemented')
|
||||||
|
->columnSpan(1),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(6)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||||
|
->label('Memory')->inlineLabel()->inline()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||||
|
->live()
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
Forms\Components\TextInput::make('memory')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
|
->label('Memory Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0)
|
||||||
|
->default(0),
|
||||||
|
Forms\Components\TextInput::make('memory_overallocate')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->label('Overallocate')->inlineLabel()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(-1)
|
||||||
|
->maxValue(100)
|
||||||
|
->default(0)
|
||||||
|
->suffix('%'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(6)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||||
|
->label('Disk')->inlineLabel()->inline()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
Forms\Components\TextInput::make('disk')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
|
->label('Disk Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0)
|
||||||
|
->default(0),
|
||||||
|
Forms\Components\TextInput::make('disk_overallocate')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
|
->label('Overallocate')->inlineLabel()
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(-1)
|
||||||
|
->maxValue(100)
|
||||||
|
->default(0)
|
||||||
|
->suffix('%'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(6)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_cpu')
|
||||||
|
->label('CPU')->inlineLabel()->inline()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
Forms\Components\TextInput::make('cpu')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||||
|
->label('CPU Limit')->inlineLabel()
|
||||||
|
->suffix('%')
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->minValue(0),
|
||||||
|
Forms\Components\TextInput::make('cpu_overallocate')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||||
|
->label('Overallocate')->inlineLabel()
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->minValue(-1)
|
||||||
|
->maxValue(100)
|
||||||
|
->suffix('%'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRedirectUrlParameters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tab' => '-configuration-tab',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
431
app/Filament/Resources/NodeResource/Pages/EditNode.php
Normal file
431
app/Filament/Resources/NodeResource/Pages/EditNode.php
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\NodeResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\NodeResource;
|
||||||
|
use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart;
|
||||||
|
use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart;
|
||||||
|
use App\Models\Node;
|
||||||
|
use App\Services\Nodes\NodeUpdateService;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Tabs;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||||
|
|
||||||
|
class EditNode extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = NodeResource::class;
|
||||||
|
|
||||||
|
public function form(Forms\Form $form): Forms\Form
|
||||||
|
{
|
||||||
|
return $form->schema([
|
||||||
|
Tabs::make('Tabs')
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 3,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 4,
|
||||||
|
])
|
||||||
|
->persistTabInQueryString()
|
||||||
|
->columnSpanFull()
|
||||||
|
->tabs([
|
||||||
|
Tabs\Tab::make('Basic Settings')
|
||||||
|
->icon('tabler-server')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('fqdn')
|
||||||
|
->columnSpan(2)
|
||||||
|
->required()
|
||||||
|
->autofocus()
|
||||||
|
->live(debounce: 1500)
|
||||||
|
->rule('prohibited', fn ($state) => is_ip($state) && request()->isSecure())
|
||||||
|
->label(fn ($state) => is_ip($state) ? 'IP Address' : 'Domain Name')
|
||||||
|
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||||
|
->helperText(function ($state) {
|
||||||
|
if (is_ip($state)) {
|
||||||
|
if (request()->isSecure()) {
|
||||||
|
return '
|
||||||
|
Your panel is currently secured via an SSL certificate and that means your nodes require one too.
|
||||||
|
You must use a domain name, because you cannot get SSL certificates for IP Addresses
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return "
|
||||||
|
This is the domain name that points to your node's IP Address.
|
||||||
|
If you've already set up this, you can verify it by checking the next field!
|
||||||
|
";
|
||||||
|
})
|
||||||
|
->hintColor('danger')
|
||||||
|
->hint(function ($state) {
|
||||||
|
if (is_ip($state) && request()->isSecure()) {
|
||||||
|
return 'You cannot connect to an IP Address over SSL';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
->afterStateUpdated(function (Forms\Set $set, ?string $state) {
|
||||||
|
$set('dns', null);
|
||||||
|
$set('ip', null);
|
||||||
|
|
||||||
|
[$subdomain] = str($state)->explode('.', 2);
|
||||||
|
if (!is_numeric($subdomain)) {
|
||||||
|
$set('name', $subdomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$state || is_ip($state)) {
|
||||||
|
$set('dns', null);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validRecords = gethostbynamel($state);
|
||||||
|
if ($validRecords) {
|
||||||
|
$set('dns', true);
|
||||||
|
|
||||||
|
$set('ip', collect($validRecords)->first());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$set('dns', false);
|
||||||
|
})
|
||||||
|
->maxLength(191),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('ip')
|
||||||
|
->disabled()
|
||||||
|
->hidden(),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('dns')
|
||||||
|
->label('DNS Record Check')
|
||||||
|
->helperText('This lets you know if your DNS record correctly points to an IP Address.')
|
||||||
|
->disabled()
|
||||||
|
->inline()
|
||||||
|
->default(null)
|
||||||
|
->hint(fn (Forms\Get $get) => $get('ip'))
|
||||||
|
->hintColor('success')
|
||||||
|
->options([
|
||||||
|
true => 'Valid',
|
||||||
|
false => 'Invalid',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'success',
|
||||||
|
false => 'danger',
|
||||||
|
])
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 1,
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('daemon_listen')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 1,
|
||||||
|
])
|
||||||
|
->label(trans('strings.port'))
|
||||||
|
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||||
|
->minValue(0)
|
||||||
|
->maxValue(65536)
|
||||||
|
->default(8080)
|
||||||
|
->required()
|
||||||
|
->integer(),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->label('Display Name')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 2,
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->regex('/[a-zA-Z0-9_\.\- ]+/')
|
||||||
|
->helperText('This name is for display only and can be changed later.')
|
||||||
|
->maxLength(100),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('scheme')
|
||||||
|
->label('Communicate over SSL')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 1,
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->inline()
|
||||||
|
->helperText(function (Forms\Get $get) {
|
||||||
|
if (request()->isSecure()) {
|
||||||
|
return new HtmlString('Your Panel is using a secure SSL connection,<br>so your Daemon must too.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_ip($get('fqdn'))) {
|
||||||
|
return 'An IP address cannot use SSL.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
->disableOptionWhen(fn (string $value): bool => $value === 'http' && request()->isSecure())
|
||||||
|
->options([
|
||||||
|
'http' => 'HTTP',
|
||||||
|
'https' => 'HTTPS (SSL)',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
'http' => 'warning',
|
||||||
|
'https' => 'success',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
'http' => 'tabler-lock-open-off',
|
||||||
|
'https' => 'tabler-lock',
|
||||||
|
])
|
||||||
|
->default(fn () => request()->isSecure() ? 'https' : 'http'), ]),
|
||||||
|
Tabs\Tab::make('Advanced Settings')
|
||||||
|
->columns(['default' => 1, 'sm' => 1, 'md' => 4, 'lg' => 6])
|
||||||
|
->icon('tabler-server-cog')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('id')
|
||||||
|
->label('Node ID')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||||
|
->disabled(),
|
||||||
|
Forms\Components\TextInput::make('uuid')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||||
|
->label('Node UUID')
|
||||||
|
->hintAction(CopyAction::make())
|
||||||
|
->disabled(),
|
||||||
|
Forms\Components\TagsInput::make('tags')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||||
|
->label('Tags')
|
||||||
|
->disabled()
|
||||||
|
->placeholder('Not Implemented')
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('Not Implemented'),
|
||||||
|
Forms\Components\TextInput::make('upload_size')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
|
||||||
|
->label('Upload Limit')
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('Enter the maximum size of files that can be uploaded through the web-based file manager.')
|
||||||
|
->numeric()->required()
|
||||||
|
->minValue(1)
|
||||||
|
->maxValue(1024)
|
||||||
|
->suffix('MiB'),
|
||||||
|
Forms\Components\ToggleButtons::make('public')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||||
|
->label('Automatic Allocation')->inline()
|
||||||
|
->options([
|
||||||
|
true => 'Yes',
|
||||||
|
false => 'No',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'success',
|
||||||
|
false => 'danger',
|
||||||
|
]),
|
||||||
|
Forms\Components\ToggleButtons::make('maintenance_mode')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||||
|
->label('Maintenance Mode')->inline()
|
||||||
|
->hinticon('tabler-question-mark')
|
||||||
|
->hintIconTooltip("If the node is marked 'Under Maintenance' users won't be able to access servers that are on this node.")
|
||||||
|
->options([
|
||||||
|
false => 'Disable',
|
||||||
|
true => 'Enable',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'success',
|
||||||
|
true => 'danger',
|
||||||
|
]),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||||
|
->label('Memory')->inlineLabel()->inline()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('memory_overallocate', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||||
|
->live()
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
||||||
|
Forms\Components\TextInput::make('memory')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
|
->label('Memory Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->required()
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
Forms\Components\TextInput::make('memory_overallocate')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->label('Overallocate')->inlineLabel()
|
||||||
|
->required()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||||
|
->numeric()
|
||||||
|
->minValue(-1)
|
||||||
|
->maxValue(100)
|
||||||
|
->suffix('%'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(['default' => 1, 'sm' => 1, 'md' => 3, 'lg' => 6])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||||
|
->label('Disk')->inlineLabel()->inline()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('disk_overallocate', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2]),
|
||||||
|
Forms\Components\TextInput::make('disk')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
|
->label('Disk Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->required()
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
Forms\Components\TextInput::make('disk_overallocate')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
|
->label('Overallocate')->inlineLabel()
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||||
|
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
|
||||||
|
->required()
|
||||||
|
->numeric()
|
||||||
|
->minValue(-1)
|
||||||
|
->maxValue(100)
|
||||||
|
->suffix('%'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(6)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_cpu')
|
||||||
|
->label('CPU')->inlineLabel()->inline()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu_overallocate', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
Forms\Components\TextInput::make('cpu')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||||
|
->label('CPU Limit')->inlineLabel()
|
||||||
|
->suffix('%')
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
Forms\Components\TextInput::make('cpu_overallocate')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||||
|
->label('Overallocate')->inlineLabel()
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('The % allowable to go over the set limit.')
|
||||||
|
->columnSpan(2)
|
||||||
|
->required()
|
||||||
|
->numeric()
|
||||||
|
->minValue(-1)
|
||||||
|
->maxValue(100)
|
||||||
|
->suffix('%'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Tabs\Tab::make('Configuration File')
|
||||||
|
->icon('tabler-code')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Placeholder::make('instructions')
|
||||||
|
->columnSpanFull()
|
||||||
|
->content(new HtmlString('
|
||||||
|
Save this file to your <span title="usually /etc/pelican/">daemon\'s root directory</span>, named <code>config.yml</code>
|
||||||
|
')),
|
||||||
|
Forms\Components\Textarea::make('config')
|
||||||
|
->label('/etc/pelican/config.yml')
|
||||||
|
->disabled()
|
||||||
|
->rows(19)
|
||||||
|
->hintAction(CopyAction::make())
|
||||||
|
->columnSpanFull(),
|
||||||
|
Forms\Components\Actions::make([
|
||||||
|
Forms\Components\Actions\Action::make('resetKey')
|
||||||
|
->label('Reset Daemon Token')
|
||||||
|
->color('danger')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading('Reset Daemon Token?')
|
||||||
|
->modalDescription('Resetting the daemon token will void any request coming from the old token. This token is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this token regularly for security.')
|
||||||
|
->action(fn (NodeUpdateService $nodeUpdateService, Node $node) => $nodeUpdateService->handle($node, [], true)
|
||||||
|
&& Notification::make()->success()->title('Daemon Key Reset')->send()
|
||||||
|
&& $this->fillForm()
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mutateFormDataBeforeFill(array $data): array
|
||||||
|
{
|
||||||
|
$node = Node::findOrFail($data['id']);
|
||||||
|
|
||||||
|
$data['config'] = $node->getYamlConfiguration();
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make()
|
||||||
|
->disabled(fn (Node $node) => $node->servers()->count() > 0)
|
||||||
|
->label(fn (Node $node) => $node->servers()->count() > 0 ? 'Node Has Servers' : 'Delete'),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFooterWidgets(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
NodeStorageChart::class,
|
||||||
|
NodeMemoryChart::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function afterSave(): void
|
||||||
|
{
|
||||||
|
$this->fillForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
107
app/Filament/Resources/NodeResource/Pages/ListNodes.php
Normal file
107
app/Filament/Resources/NodeResource/Pages/ListNodes.php
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\NodeResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\NodeResource;
|
||||||
|
use App\Models\Node;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Actions\CreateAction;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables;
|
||||||
|
|
||||||
|
class ListNodes extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = NodeResource::class;
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->checkIfRecordIsSelectableUsing(fn (Node $node) => $node->servers_count <= 0)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('uuid')
|
||||||
|
->label('UUID')
|
||||||
|
->searchable()
|
||||||
|
->hidden(),
|
||||||
|
Tables\Columns\IconColumn::make('health')
|
||||||
|
->alignCenter()
|
||||||
|
->state(fn (Node $node) => $node)
|
||||||
|
->view('livewire.columns.version-column'),
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->icon('tabler-server-2')
|
||||||
|
->sortable()
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('fqdn')
|
||||||
|
->visibleFrom('md')
|
||||||
|
->label('Address')
|
||||||
|
->icon('tabler-network')
|
||||||
|
->sortable()
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('memory')
|
||||||
|
->visibleFrom('sm')
|
||||||
|
->icon('tabler-device-desktop-analytics')
|
||||||
|
->numeric()
|
||||||
|
->suffix(' GiB')
|
||||||
|
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('disk')
|
||||||
|
->visibleFrom('sm')
|
||||||
|
->icon('tabler-file')
|
||||||
|
->numeric()
|
||||||
|
->suffix(' GiB')
|
||||||
|
->formatStateUsing(fn ($state) => number_format($state / 1024, 2))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('cpu')
|
||||||
|
->visibleFrom('sm')
|
||||||
|
->icon('tabler-file')
|
||||||
|
->numeric()
|
||||||
|
->suffix(' %')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\IconColumn::make('scheme')
|
||||||
|
->visibleFrom('xl')
|
||||||
|
->label('SSL')
|
||||||
|
->trueIcon('tabler-lock')
|
||||||
|
->falseIcon('tabler-lock-open-off')
|
||||||
|
->state(fn (Node $node) => $node->scheme === 'https'),
|
||||||
|
Tables\Columns\IconColumn::make('public')
|
||||||
|
->visibleFrom('lg')
|
||||||
|
->trueIcon('tabler-eye-check')
|
||||||
|
->falseIcon('tabler-eye-cancel'),
|
||||||
|
Tables\Columns\TextColumn::make('servers_count')
|
||||||
|
->visibleFrom('sm')
|
||||||
|
->counts('servers')
|
||||||
|
->label('Servers')
|
||||||
|
->sortable()
|
||||||
|
->icon('tabler-brand-docker'),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->emptyStateIcon('tabler-server-2')
|
||||||
|
->emptyStateDescription('')
|
||||||
|
->emptyStateHeading('No Nodes')
|
||||||
|
->emptyStateActions([
|
||||||
|
CreateAction::make('create')
|
||||||
|
->label('Create Node')
|
||||||
|
->button(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make()
|
||||||
|
->label('Create Node')
|
||||||
|
->hidden(fn () => Node::count() <= 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||||
|
|
||||||
|
use App\Models\Allocation;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Services\Allocations\AssignmentService;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
|
||||||
|
class AllocationsRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'allocations';
|
||||||
|
|
||||||
|
protected static ?string $icon = 'tabler-plug-connected';
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('ip')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->recordTitleAttribute('ip')
|
||||||
|
|
||||||
|
// Non Primary Allocations
|
||||||
|
// ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
|
||||||
|
|
||||||
|
// All assigned allocations
|
||||||
|
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||||
|
->searchable()
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('id'),
|
||||||
|
Tables\Columns\TextColumn::make('port')
|
||||||
|
->searchable()
|
||||||
|
->label('Port'),
|
||||||
|
Tables\Columns\TextColumn::make('server.name')
|
||||||
|
->label('Server')
|
||||||
|
->icon('tabler-brand-docker')
|
||||||
|
->searchable()
|
||||||
|
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
|
||||||
|
Tables\Columns\TextInputColumn::make('ip_alias')
|
||||||
|
->searchable()
|
||||||
|
->label('Alias'),
|
||||||
|
Tables\Columns\TextInputColumn::make('ip')
|
||||||
|
->searchable()
|
||||||
|
->label('IP'),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->headerActions([
|
||||||
|
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
|
||||||
|
->form(fn () => [
|
||||||
|
Forms\Components\TextInput::make('allocation_ip')
|
||||||
|
->datalist($this->getOwnerRecord()->ipAddresses() ?? [])
|
||||||
|
->label('IP Address')
|
||||||
|
->inlineLabel()
|
||||||
|
->ipv4()
|
||||||
|
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('allocation_alias')
|
||||||
|
->label('Alias')
|
||||||
|
->inlineLabel()
|
||||||
|
->default(null)
|
||||||
|
->helperText('Optional display name to help you remember what these are.')
|
||||||
|
->required(false),
|
||||||
|
Forms\Components\TagsInput::make('allocation_ports')
|
||||||
|
->placeholder('Examples: 27015, 27017-27019')
|
||||||
|
->helperText(new HtmlString('
|
||||||
|
These are the ports that users can connect to this Server through.
|
||||||
|
<br />
|
||||||
|
You would have to port forward these on your home network.
|
||||||
|
'))
|
||||||
|
->label('Ports')
|
||||||
|
->inlineLabel()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||||
|
$ports = collect();
|
||||||
|
$update = false;
|
||||||
|
foreach ($state as $portEntry) {
|
||||||
|
if (!str_contains($portEntry, '-')) {
|
||||||
|
if (is_numeric($portEntry)) {
|
||||||
|
$ports->push((int) $portEntry);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not add non numerical ports
|
||||||
|
$update = true;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = true;
|
||||||
|
[$start, $end] = explode('-', $portEntry);
|
||||||
|
if (!is_numeric($start) || !is_numeric($end)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = max((int) $start, 0);
|
||||||
|
$end = min((int) $end, 2 ** 16 - 1);
|
||||||
|
for ($i = $start; $i <= $end; $i++) {
|
||||||
|
$ports->push($i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$uniquePorts = $ports->unique()->values();
|
||||||
|
if ($ports->count() > $uniquePorts->count()) {
|
||||||
|
$update = true;
|
||||||
|
$ports = $uniquePorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortedPorts = $ports->sort()->values();
|
||||||
|
if ($sortedPorts->all() !== $ports->all()) {
|
||||||
|
$update = true;
|
||||||
|
$ports = $sortedPorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||||
|
|
||||||
|
if ($update) {
|
||||||
|
$set('allocation_ports', $ports->all());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->splitKeys(['Tab', ' ', ','])
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord(), $data)),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
// Tables\Actions\DissociateBulkAction::make(),
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
|
||||||
|
class NodesRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'servers';
|
||||||
|
|
||||||
|
protected static ?string $icon = 'tabler-brand-docker';
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('user.username')
|
||||||
|
->label('Owner')
|
||||||
|
->icon('tabler-user')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->icon('tabler-brand-docker')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('egg.name')
|
||||||
|
->icon('tabler-egg')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user]))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\SelectColumn::make('allocation.id')
|
||||||
|
->label('Primary Allocation')
|
||||||
|
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||||
|
->selectablePlaceholder(false)
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
|
||||||
|
Tables\Columns\TextColumn::make('cpu')->icon('tabler-cpu'),
|
||||||
|
Tables\Columns\TextColumn::make('databases_count')
|
||||||
|
->counts('databases')
|
||||||
|
->label('Databases')
|
||||||
|
->icon('tabler-database')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('backups_count')
|
||||||
|
->counts('backups')
|
||||||
|
->label('Backups')
|
||||||
|
->icon('tabler-file-download')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||||
|
|
||||||
|
use App\Models\Node;
|
||||||
|
use Filament\Widgets\ChartWidget;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class NodeMemoryChart extends ChartWidget
|
||||||
|
{
|
||||||
|
protected static ?string $heading = 'Memory';
|
||||||
|
|
||||||
|
protected static ?string $pollingInterval = '60s';
|
||||||
|
|
||||||
|
public ?Model $record = null;
|
||||||
|
|
||||||
|
protected static ?array $options = [
|
||||||
|
'scales' => [
|
||||||
|
'x' => [
|
||||||
|
'grid' => [
|
||||||
|
'display' => false,
|
||||||
|
],
|
||||||
|
'ticks' => [
|
||||||
|
'display' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'y' => [
|
||||||
|
'grid' => [
|
||||||
|
'display' => false,
|
||||||
|
],
|
||||||
|
'ticks' => [
|
||||||
|
'display' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function getData(): array
|
||||||
|
{
|
||||||
|
/** @var Node $node */
|
||||||
|
$node = $this->record;
|
||||||
|
|
||||||
|
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||||
|
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||||
|
$unused = $total - $used;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'datasets' => [
|
||||||
|
[
|
||||||
|
'label' => 'Data Cool',
|
||||||
|
'data' => [$used, $unused],
|
||||||
|
'backgroundColor' => [
|
||||||
|
'rgb(255, 99, 132)',
|
||||||
|
'rgb(54, 162, 235)',
|
||||||
|
'rgb(255, 205, 86)',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// 'backgroundColor' => [],
|
||||||
|
],
|
||||||
|
'labels' => ['Used', 'Unused'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getType(): string
|
||||||
|
{
|
||||||
|
return 'pie';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||||
|
|
||||||
|
use App\Models\Node;
|
||||||
|
use Filament\Widgets\ChartWidget;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class NodeStorageChart extends ChartWidget
|
||||||
|
{
|
||||||
|
protected static ?string $heading = 'Storage';
|
||||||
|
|
||||||
|
protected static ?string $pollingInterval = '60s';
|
||||||
|
|
||||||
|
public ?Model $record = null;
|
||||||
|
|
||||||
|
protected static ?array $options = [
|
||||||
|
'scales' => [
|
||||||
|
'x' => [
|
||||||
|
'grid' => [
|
||||||
|
'display' => false,
|
||||||
|
],
|
||||||
|
'ticks' => [
|
||||||
|
'display' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'y' => [
|
||||||
|
'grid' => [
|
||||||
|
'display' => false,
|
||||||
|
],
|
||||||
|
'ticks' => [
|
||||||
|
'display' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function getData(): array
|
||||||
|
{
|
||||||
|
/** @var Node $node */
|
||||||
|
$node = $this->record;
|
||||||
|
|
||||||
|
$total = ($node->statistics()['disk_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||||
|
$used = ($node->statistics()['disk_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||||
|
$unused = $total - $used;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'datasets' => [
|
||||||
|
[
|
||||||
|
'label' => 'Data Cool',
|
||||||
|
'data' => [$used, $unused],
|
||||||
|
'backgroundColor' => [
|
||||||
|
'rgb(255, 99, 132)',
|
||||||
|
'rgb(54, 162, 235)',
|
||||||
|
'rgb(255, 205, 86)',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// 'backgroundColor' => [],
|
||||||
|
],
|
||||||
|
'labels' => ['Used', 'Unused'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getType(): string
|
||||||
|
{
|
||||||
|
return 'pie';
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Filament/Resources/ServerResource.php
Normal file
37
app/Filament/Resources/ServerResource.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ServerResource\Pages;
|
||||||
|
use App\Models\Server;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
|
||||||
|
class ServerResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Server::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-brand-docker';
|
||||||
|
|
||||||
|
protected static ?string $recordTitleAttribute = 'name';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListServers::route('/'),
|
||||||
|
'create' => Pages\CreateServer::route('/create'),
|
||||||
|
'edit' => Pages\EditServer::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
750
app/Filament/Resources/ServerResource/Pages/CreateServer.php
Normal file
750
app/Filament/Resources/ServerResource/Pages/CreateServer.php
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ServerResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ServerResource;
|
||||||
|
use App\Models\Allocation;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use App\Models\Node;
|
||||||
|
use App\Services\Allocations\AssignmentService;
|
||||||
|
use App\Services\Servers\RandomWordService;
|
||||||
|
use App\Services\Servers\ServerCreationService;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Closure;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
|
||||||
|
class CreateServer extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = ServerResource::class;
|
||||||
|
protected static bool $canCreateAnother = false;
|
||||||
|
|
||||||
|
public ?Node $node = null;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('external_id')
|
||||||
|
->maxLength(191)
|
||||||
|
->hidden(),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->prefixIcon('tabler-server')
|
||||||
|
->label('Display Name')
|
||||||
|
->suffixAction(Forms\Components\Actions\Action::make('random')
|
||||||
|
->icon('tabler-dice-' . random_int(1, 6))
|
||||||
|
->action(function (Forms\Set $set, Forms\Get $get) {
|
||||||
|
$egg = Egg::find($get('egg_id'));
|
||||||
|
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
|
||||||
|
|
||||||
|
$word = (new RandomWordService())->word();
|
||||||
|
|
||||||
|
$set('name', $prefix . $word);
|
||||||
|
}))
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('owner_id')
|
||||||
|
->prefixIcon('tabler-user')
|
||||||
|
->default(auth()->user()->id)
|
||||||
|
->label('Owner')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->relationship('user', 'username')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('node_id')
|
||||||
|
->disabledOn('edit')
|
||||||
|
->prefixIcon('tabler-server-2')
|
||||||
|
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
|
||||||
|
->columnSpan(2)
|
||||||
|
->live()
|
||||||
|
->relationship('node', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->afterStateUpdated(function (Forms\Set $set, $state) {
|
||||||
|
$set('allocation_id', null);
|
||||||
|
$this->node = Node::find($state);
|
||||||
|
})
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('allocation_id')
|
||||||
|
->preload()
|
||||||
|
->live()
|
||||||
|
->prefixIcon('tabler-network')
|
||||||
|
->label('Primary Allocation')
|
||||||
|
->columnSpan(2)
|
||||||
|
->disabled(fn (Forms\Get $get) => $get('node_id') === null)
|
||||||
|
->searchable(['ip', 'port', 'ip_alias'])
|
||||||
|
->afterStateUpdated(function (Forms\Set $set) {
|
||||||
|
$set('allocation_additional', null);
|
||||||
|
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
|
||||||
|
})
|
||||||
|
->getOptionLabelFromRecordUsing(
|
||||||
|
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||||
|
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||||
|
)
|
||||||
|
->placeholder(function (Forms\Get $get) {
|
||||||
|
$node = Node::find($get('node_id'));
|
||||||
|
|
||||||
|
if ($node?->allocations) {
|
||||||
|
return 'Select an Allocation';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Create a New Allocation';
|
||||||
|
})
|
||||||
|
->relationship(
|
||||||
|
'allocation',
|
||||||
|
'ip',
|
||||||
|
fn (Builder $query, Forms\Get $get) => $query
|
||||||
|
->where('node_id', $get('node_id'))
|
||||||
|
->whereNull('server_id'),
|
||||||
|
)
|
||||||
|
->createOptionForm(fn (Forms\Get $get) => [
|
||||||
|
Forms\Components\TextInput::make('allocation_ip')
|
||||||
|
->datalist(Node::find($get('node_id'))?->ipAddresses() ?? [])
|
||||||
|
->label('IP Address')
|
||||||
|
->inlineLabel()
|
||||||
|
->ipv4()
|
||||||
|
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||||
|
// ->selectablePlaceholder(false)
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('allocation_alias')
|
||||||
|
->label('Alias')
|
||||||
|
->inlineLabel()
|
||||||
|
->default(null)
|
||||||
|
->datalist([
|
||||||
|
$get('name'),
|
||||||
|
Egg::find($get('egg_id'))?->name,
|
||||||
|
])
|
||||||
|
->helperText('Optional display name to help you remember what these are.')
|
||||||
|
->required(false),
|
||||||
|
Forms\Components\TagsInput::make('allocation_ports')
|
||||||
|
->placeholder('Examples: 27015, 27017-27019')
|
||||||
|
->helperText(new HtmlString('
|
||||||
|
These are the ports that users can connect to this Server through.
|
||||||
|
<br />
|
||||||
|
You would have to port forward these on your home network.
|
||||||
|
'))
|
||||||
|
->label('Ports')
|
||||||
|
->inlineLabel()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||||
|
$ports = collect();
|
||||||
|
$update = false;
|
||||||
|
foreach ($state as $portEntry) {
|
||||||
|
if (!str_contains($portEntry, '-')) {
|
||||||
|
if (is_numeric($portEntry)) {
|
||||||
|
$ports->push((int) $portEntry);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not add non-numerical ports
|
||||||
|
$update = true;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = true;
|
||||||
|
[$start, $end] = explode('-', $portEntry);
|
||||||
|
if (!is_numeric($start) || !is_numeric($end)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start = max((int) $start, 0);
|
||||||
|
$end = min((int) $end, 2 ** 16 - 1);
|
||||||
|
for ($i = $start; $i <= $end; $i++) {
|
||||||
|
$ports->push($i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$uniquePorts = $ports->unique()->values();
|
||||||
|
if ($ports->count() > $uniquePorts->count()) {
|
||||||
|
$update = true;
|
||||||
|
$ports = $uniquePorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortedPorts = $ports->sort()->values();
|
||||||
|
if ($sortedPorts->all() !== $ports->all()) {
|
||||||
|
$update = true;
|
||||||
|
$ports = $sortedPorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||||
|
|
||||||
|
if ($update) {
|
||||||
|
$set('allocation_ports', $ports->all());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->splitKeys(['Tab', ' ', ','])
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->createOptionUsing(function (array $data, Forms\Get $get): int {
|
||||||
|
return collect(
|
||||||
|
resolve(AssignmentService::class)->handle(Node::find($get('node_id')), $data)
|
||||||
|
)->first();
|
||||||
|
})
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Repeater::make('allocation_additional')
|
||||||
|
->label('Additional Allocations')
|
||||||
|
->columnSpan(2)
|
||||||
|
->addActionLabel('Add Allocation')
|
||||||
|
->disabled(fn (Forms\Get $get) => $get('allocation_id') === null)
|
||||||
|
// ->addable() TODO disable when all allocations are taken
|
||||||
|
// ->addable() TODO disable until first additional allocation is selected
|
||||||
|
->simple(
|
||||||
|
Forms\Components\Select::make('extra_allocations')
|
||||||
|
->live()
|
||||||
|
->preload()
|
||||||
|
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||||
|
->prefixIcon('tabler-network')
|
||||||
|
->label('Additional Allocations')
|
||||||
|
->columnSpan(2)
|
||||||
|
->disabled(fn (Forms\Get $get) => $get('../../node_id') === null)
|
||||||
|
->searchable(['ip', 'port', 'ip_alias'])
|
||||||
|
->getOptionLabelFromRecordUsing(
|
||||||
|
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||||
|
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||||
|
)
|
||||||
|
->placeholder('Select additional Allocations')
|
||||||
|
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||||
|
->relationship(
|
||||||
|
'allocations',
|
||||||
|
'ip',
|
||||||
|
fn (Builder $query, Forms\Get $get, Forms\Components\Select $component, $state) => $query
|
||||||
|
->where('node_id', $get('../../node_id'))
|
||||||
|
->whereNot('id', $get('../../allocation_id'))
|
||||||
|
->whereNull('server_id'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->hidden()
|
||||||
|
->default('')
|
||||||
|
->required()
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('egg_id')
|
||||||
|
->disabledOn('edit')
|
||||||
|
->prefixIcon('tabler-egg')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 5,
|
||||||
|
])
|
||||||
|
->relationship('egg', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(function ($state, Forms\Set $set, Forms\Get $get, $old) {
|
||||||
|
$egg = Egg::query()->find($state);
|
||||||
|
$set('startup', $egg->startup);
|
||||||
|
$set('image', '');
|
||||||
|
|
||||||
|
$variables = $egg->variables ?? [];
|
||||||
|
$serverVariables = collect();
|
||||||
|
foreach ($variables as $variable) {
|
||||||
|
$serverVariables->add($variable->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
$variables = [];
|
||||||
|
$set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all());
|
||||||
|
for ($i = 0; $i < $serverVariables->count(); $i++) {
|
||||||
|
$set("$path.$i.variable_value", $serverVariables[$i]['default_value']);
|
||||||
|
$set("$path.$i.variable_id", $serverVariables[$i]['id']);
|
||||||
|
$variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$set('environment', $variables);
|
||||||
|
|
||||||
|
$previousEgg = Egg::query()->find($old);
|
||||||
|
if (!$get('name') || $previousEgg?->getKebabName() === $get('name')) {
|
||||||
|
$set('name', $egg->getKebabName());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||||
|
->label('Run Egg Install Script?')
|
||||||
|
->default(false)
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 1,
|
||||||
|
'md' => 1,
|
||||||
|
'lg' => 1,
|
||||||
|
])
|
||||||
|
->options([
|
||||||
|
false => 'Yes',
|
||||||
|
true => 'Skip',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'primary',
|
||||||
|
true => 'danger',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
false => 'tabler-code',
|
||||||
|
true => 'tabler-code-off',
|
||||||
|
])
|
||||||
|
->inline()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Textarea::make('startup')
|
||||||
|
->hintIcon('tabler-code')
|
||||||
|
->label('Startup Command')
|
||||||
|
->required()
|
||||||
|
->live()
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->rows(function ($state) {
|
||||||
|
return str($state)->explode("\n")->reduce(
|
||||||
|
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('environment')->default([]),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('start_on_completion')->default(true),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Egg Variables')
|
||||||
|
->icon('tabler-eggs')
|
||||||
|
->iconColor('primary')
|
||||||
|
->collapsible()
|
||||||
|
->collapsed()
|
||||||
|
->columnSpan(([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
]))
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Placeholder::make('Select an egg first to show its variables!')
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('egg_id')),
|
||||||
|
|
||||||
|
Forms\Components\Placeholder::make('The selected egg has no variables!')
|
||||||
|
->hidden(fn (Forms\Get $get) => !$get('egg_id') ||
|
||||||
|
Egg::query()->find($get('egg_id'))?->variables()?->count()
|
||||||
|
),
|
||||||
|
|
||||||
|
Forms\Components\Repeater::make('server_variables')
|
||||||
|
->relationship('serverVariables')
|
||||||
|
->saveRelationshipsBeforeChildrenUsing(null)
|
||||||
|
->saveRelationshipsUsing(null)
|
||||||
|
->grid(2)
|
||||||
|
->reorderable(false)
|
||||||
|
->addable(false)
|
||||||
|
->deletable(false)
|
||||||
|
->default([])
|
||||||
|
->hidden(fn ($state) => empty($state))
|
||||||
|
->schema(function () {
|
||||||
|
|
||||||
|
$text = Forms\Components\TextInput::make('variable_value')
|
||||||
|
->hidden($this->shouldHideComponent(...))
|
||||||
|
->maxLength(191)
|
||||||
|
->rules([
|
||||||
|
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||||
|
$validator = Validator::make(['validatorkey' => $value], [
|
||||||
|
'validatorkey' => $get('rules'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'));
|
||||||
|
|
||||||
|
$fail($message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$select = Forms\Components\Select::make('variable_value')
|
||||||
|
->hidden($this->shouldHideComponent(...))
|
||||||
|
->options($this->getSelectOptionsFromRules(...))
|
||||||
|
->selectablePlaceholder(false);
|
||||||
|
|
||||||
|
$components = [$text, $select];
|
||||||
|
|
||||||
|
/** @var Forms\Components\Component $component */
|
||||||
|
foreach ($components as &$component) {
|
||||||
|
$component = $component
|
||||||
|
->live(onBlur: true)
|
||||||
|
->hintIcon('tabler-code')
|
||||||
|
->label(fn (Forms\Get $get) => $get('name'))
|
||||||
|
->hintIconTooltip(fn (Forms\Get $get) => $get('rules'))
|
||||||
|
->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}')
|
||||||
|
->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description'))
|
||||||
|
->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
||||||
|
$environment = $get($envPath = '../../environment');
|
||||||
|
$environment[$get('env_variable')] = $state;
|
||||||
|
$set($envPath, $environment);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $components;
|
||||||
|
})
|
||||||
|
->columnSpan(2),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Environment Management')
|
||||||
|
->collapsed()
|
||||||
|
->icon('tabler-server-cog')
|
||||||
|
->iconColor('primary')
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 4,
|
||||||
|
])
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Fieldset::make('Resource Limits')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||||
|
->label('Memory')->inlineLabel()->inline()
|
||||||
|
->default(true)
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||||
|
->live()
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('memory')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
|
->label('Memory Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->default(0)
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||||
|
->label('Disk Space')->inlineLabel()->inline()
|
||||||
|
->default(true)
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('disk')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
|
->label('Disk Space Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->default(0)
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_cpu')
|
||||||
|
->label('CPU')->inlineLabel()->inline()
|
||||||
|
->default(true)
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
|
||||||
|
->live()
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('cpu')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||||
|
->label('CPU Limit')->inlineLabel()
|
||||||
|
->suffix('%')
|
||||||
|
->default(0)
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0)
|
||||||
|
->helperText('100% equals one CPU core.'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('swap_support')
|
||||||
|
->live()
|
||||||
|
->label('Enable Swap Memory')
|
||||||
|
->inlineLabel()
|
||||||
|
->inline()
|
||||||
|
->columnSpan(2)
|
||||||
|
->default('disabled')
|
||||||
|
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||||
|
$value = match ($state) {
|
||||||
|
'unlimited' => -1,
|
||||||
|
'disabled' => 0,
|
||||||
|
'limited' => 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
$set('swap', $value);
|
||||||
|
})
|
||||||
|
->options([
|
||||||
|
'unlimited' => 'Unlimited',
|
||||||
|
'limited' => 'Limited',
|
||||||
|
'disabled' => 'Disabled',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
'unlimited' => 'primary',
|
||||||
|
'limited' => 'warning',
|
||||||
|
'disabled' => 'danger',
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('swap')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
|
||||||
|
'disabled', 'unlimited' => true,
|
||||||
|
'limited' => false,
|
||||||
|
})
|
||||||
|
->label('Swap Memory')
|
||||||
|
->default(0)
|
||||||
|
->suffix('MiB')
|
||||||
|
->minValue(-1)
|
||||||
|
->columnSpan(2)
|
||||||
|
->inlineLabel()
|
||||||
|
->required()
|
||||||
|
->integer(),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('io')
|
||||||
|
->helperText('The IO performance relative to other running containers')
|
||||||
|
->label('Block IO Proportion')
|
||||||
|
->default(500),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('oom_killer')
|
||||||
|
->label('OOM Killer')
|
||||||
|
->inlineLabel()->inline()
|
||||||
|
->default(false)
|
||||||
|
->columnSpan(2)
|
||||||
|
->options([
|
||||||
|
false => 'Disabled',
|
||||||
|
true => 'Enabled',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'success',
|
||||||
|
true => 'danger',
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('oom_disabled_hidden')
|
||||||
|
->hidden(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Fieldset::make('Feature Limits')
|
||||||
|
->inlineLabel()
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('allocation_limit')
|
||||||
|
->suffixIcon('tabler-network')
|
||||||
|
->required()
|
||||||
|
->numeric()
|
||||||
|
->default(0),
|
||||||
|
Forms\Components\TextInput::make('database_limit')
|
||||||
|
->suffixIcon('tabler-database')
|
||||||
|
->required()
|
||||||
|
->numeric()
|
||||||
|
->default(0),
|
||||||
|
Forms\Components\TextInput::make('backup_limit')
|
||||||
|
->suffixIcon('tabler-copy-check')
|
||||||
|
->required()
|
||||||
|
->numeric()
|
||||||
|
->default(0),
|
||||||
|
]),
|
||||||
|
Forms\Components\Fieldset::make('Docker Settings')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('select_image')
|
||||||
|
->label('Image Name')
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
|
||||||
|
->options(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||||
|
$egg = Egg::query()->find($get('egg_id'));
|
||||||
|
$images = $egg->docker_images ?? [];
|
||||||
|
|
||||||
|
$currentImage = $get('image');
|
||||||
|
if (!$currentImage && $images) {
|
||||||
|
$defaultImage = collect($images)->first();
|
||||||
|
$set('image', $defaultImage);
|
||||||
|
$set('select_image', $defaultImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
|
||||||
|
})
|
||||||
|
->selectablePlaceholder(false)
|
||||||
|
->columnSpan(1),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('image')
|
||||||
|
->label('Image')
|
||||||
|
->debounce(500)
|
||||||
|
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||||
|
$egg = Egg::query()->find($get('egg_id'));
|
||||||
|
$images = $egg->docker_images ?? [];
|
||||||
|
|
||||||
|
if (in_array($state, $images)) {
|
||||||
|
$set('select_image', $state);
|
||||||
|
} else {
|
||||||
|
$set('select_image', 'ghcr.io/custom-image');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->placeholder('Enter a custom Image')
|
||||||
|
->columnSpan(1),
|
||||||
|
|
||||||
|
Forms\Components\KeyValue::make('docker_labels')
|
||||||
|
->label('Container Labels')
|
||||||
|
->keyLabel('Title')
|
||||||
|
->valueLabel('Description')
|
||||||
|
->columnSpan(3),
|
||||||
|
|
||||||
|
Forms\Components\CheckboxList::make('mounts')
|
||||||
|
->live()
|
||||||
|
->relationship('mounts')
|
||||||
|
->options(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]) ?? [])
|
||||||
|
->descriptions(fn () => $this->node?->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]) ?? [])
|
||||||
|
->label('Mounts')
|
||||||
|
->helperText(fn () => $this->node?->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function handleRecordCreation(array $data): Model
|
||||||
|
{
|
||||||
|
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
|
||||||
|
|
||||||
|
/** @var ServerCreationService $service */
|
||||||
|
$service = resolve(ServerCreationService::class);
|
||||||
|
|
||||||
|
return $service->handle($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||||
|
{
|
||||||
|
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||||
|
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($component instanceof Forms\Components\Select) {
|
||||||
|
return $containsRuleIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($component instanceof Forms\Components\TextInput) {
|
||||||
|
return !$containsRuleIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception('Component type not supported: ' . $component::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||||
|
{
|
||||||
|
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||||
|
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||||
|
);
|
||||||
|
|
||||||
|
return str($inRule)
|
||||||
|
->after('in:')
|
||||||
|
->explode(',')
|
||||||
|
->each(fn ($value) => str($value)->trim())
|
||||||
|
->mapWithKeys(fn ($value) => [$value => $value])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
785
app/Filament/Resources/ServerResource/Pages/EditServer.php
Normal file
785
app/Filament/Resources/ServerResource/Pages/EditServer.php
Normal file
@@ -0,0 +1,785 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ServerResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ServerResource;
|
||||||
|
use App\Http\Controllers\Admin\ServersController;
|
||||||
|
use App\Services\Servers\RandomWordService;
|
||||||
|
use App\Services\Servers\SuspensionService;
|
||||||
|
use App\Services\Servers\TransferServerService;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Forms;
|
||||||
|
use App\Enums\ContainerStatus;
|
||||||
|
use App\Enums\ServerState;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\ServerVariable;
|
||||||
|
use App\Repositories\Daemon\DaemonServerRepository;
|
||||||
|
use App\Services\Servers\ServerDeletionService;
|
||||||
|
use Filament\Forms\Components\Tabs;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Closure;
|
||||||
|
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||||
|
|
||||||
|
class EditServer extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = ServerResource::class;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 4,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('docker')
|
||||||
|
->label('Container Status')->inline()->inlineLabel()
|
||||||
|
->formatStateUsing(function ($state, Server $server) {
|
||||||
|
if ($server->node_id === null) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var DaemonServerRepository $service */
|
||||||
|
$service = resolve(DaemonServerRepository::class);
|
||||||
|
$details = $service->setServer($server)->getDetails();
|
||||||
|
|
||||||
|
return $details['state'] ?? 'unknown';
|
||||||
|
})
|
||||||
|
->options(fn ($state) => collect(ContainerStatus::cases())->filter(fn ($containerStatus) => $containerStatus->value === $state)->mapWithKeys(
|
||||||
|
fn (ContainerStatus $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
|
||||||
|
))
|
||||||
|
->colors(collect(ContainerStatus::cases())->mapWithKeys(
|
||||||
|
fn (ContainerStatus $status) => [$status->value => $status->color()]
|
||||||
|
))
|
||||||
|
->icons(collect(ContainerStatus::cases())->mapWithKeys(
|
||||||
|
fn (ContainerStatus $status) => [$status->value => $status->icon()]
|
||||||
|
))
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 2,
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('status')
|
||||||
|
->label('Server State')->inline()->inlineLabel()
|
||||||
|
->helperText('')
|
||||||
|
->formatStateUsing(fn ($state) => $state ?? ServerState::Normal)
|
||||||
|
->options(fn ($state) => collect(ServerState::cases())->filter(fn ($serverState) => $serverState->value === $state)->mapWithKeys(
|
||||||
|
fn (ServerState $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
|
||||||
|
))
|
||||||
|
->colors(collect(ServerState::cases())->mapWithKeys(
|
||||||
|
fn (ServerState $state) => [$state->value => $state->color()]
|
||||||
|
))
|
||||||
|
->icons(collect(ServerState::cases())->mapWithKeys(
|
||||||
|
fn (ServerState $state) => [$state->value => $state->icon()]
|
||||||
|
))
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 2,
|
||||||
|
]),
|
||||||
|
|
||||||
|
Tabs::make('Tabs')
|
||||||
|
->persistTabInQueryString()
|
||||||
|
->columnSpan(6)
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->tabs([
|
||||||
|
Tabs\Tab::make('Information')
|
||||||
|
->icon('tabler-info-circle')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->prefixIcon('tabler-server')
|
||||||
|
->label('Display Name')
|
||||||
|
->suffixAction(Forms\Components\Actions\Action::make('random')
|
||||||
|
->icon('tabler-dice-' . random_int(1, 6))
|
||||||
|
->action(function (Forms\Set $set, Forms\Get $get) {
|
||||||
|
$egg = Egg::find($get('egg_id'));
|
||||||
|
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
|
||||||
|
|
||||||
|
$word = (new RandomWordService())->word();
|
||||||
|
|
||||||
|
$set('name', $prefix . $word);
|
||||||
|
}))
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('owner_id')
|
||||||
|
->prefixIcon('tabler-user')
|
||||||
|
->label('Owner')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->relationship('user', 'username')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->label('Description')
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('uuid')
|
||||||
|
->hintAction(CopyAction::make())
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->readOnly(),
|
||||||
|
Forms\Components\TextInput::make('uuid_short')
|
||||||
|
->label('Short UUID')
|
||||||
|
->hintAction(CopyAction::make())
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->readOnly(),
|
||||||
|
Forms\Components\TextInput::make('external_id')
|
||||||
|
->label('External ID')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\Select::make('node_id')
|
||||||
|
->label('Node')
|
||||||
|
->relationship('node', 'name')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->disabled(),
|
||||||
|
]),
|
||||||
|
Tabs\Tab::make('Environment')
|
||||||
|
->icon('tabler-brand-docker')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Fieldset::make('Resource Limits')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||||
|
->label('Memory')->inlineLabel()->inline()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||||
|
->live()
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('memory')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
|
->label('Memory Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||||
|
->label('Disk Space')->inlineLabel()->inline()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('disk')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
|
->label('Disk Space Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_cpu')
|
||||||
|
->label('CPU')->inlineLabel()->inline()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
|
||||||
|
->live()
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('cpu')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||||
|
->label('CPU Limit')->inlineLabel()
|
||||||
|
->suffix('%')
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('swap_support')
|
||||||
|
->live()
|
||||||
|
->label('Enable Swap Memory')->inlineLabel()->inline()
|
||||||
|
->columnSpan(2)
|
||||||
|
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||||
|
$value = match ($state) {
|
||||||
|
'unlimited' => -1,
|
||||||
|
'disabled' => 0,
|
||||||
|
'limited' => 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
$set('swap', $value);
|
||||||
|
})
|
||||||
|
->formatStateUsing(function (Forms\Get $get) {
|
||||||
|
return match (true) {
|
||||||
|
$get('swap') > 0 => 'limited',
|
||||||
|
$get('swap') == 0 => 'disabled',
|
||||||
|
$get('swap') < 0 => 'unlimited',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
->options([
|
||||||
|
'unlimited' => 'Unlimited',
|
||||||
|
'limited' => 'Limited',
|
||||||
|
'disabled' => 'Disabled',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
'unlimited' => 'primary',
|
||||||
|
'limited' => 'warning',
|
||||||
|
'disabled' => 'danger',
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('swap')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
|
||||||
|
'disabled', 'unlimited', true => true,
|
||||||
|
'limited', false => false,
|
||||||
|
})
|
||||||
|
->label('Swap Memory')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->minValue(-1)
|
||||||
|
->columnSpan(2)
|
||||||
|
->required()
|
||||||
|
->integer(),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('io')
|
||||||
|
->helperText('The IO performance relative to other running containers')
|
||||||
|
->label('Block IO Proportion'),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('oom_killer')
|
||||||
|
->label('OOM Killer')->inlineLabel()->inline()
|
||||||
|
->columnSpan(2)
|
||||||
|
->options([
|
||||||
|
false => 'Disabled',
|
||||||
|
true => 'Enabled',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'success',
|
||||||
|
true => 'danger',
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('oom_disabled_hidden')
|
||||||
|
->hidden(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Fieldset::make('Feature Limits')
|
||||||
|
->inlineLabel()
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('allocation_limit')
|
||||||
|
->suffixIcon('tabler-network')
|
||||||
|
->required()
|
||||||
|
->numeric(),
|
||||||
|
Forms\Components\TextInput::make('database_limit')
|
||||||
|
->suffixIcon('tabler-database')
|
||||||
|
->required()
|
||||||
|
->numeric(),
|
||||||
|
Forms\Components\TextInput::make('backup_limit')
|
||||||
|
->suffixIcon('tabler-copy-check')
|
||||||
|
->required()
|
||||||
|
->numeric(),
|
||||||
|
]),
|
||||||
|
Forms\Components\Fieldset::make('Docker Settings')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('select_image')
|
||||||
|
->label('Image Name')
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
|
||||||
|
->options(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||||
|
$egg = Egg::query()->find($get('egg_id'));
|
||||||
|
$images = $egg->docker_images ?? [];
|
||||||
|
|
||||||
|
$currentImage = $get('image');
|
||||||
|
if (!$currentImage && $images) {
|
||||||
|
$defaultImage = collect($images)->first();
|
||||||
|
$set('image', $defaultImage);
|
||||||
|
$set('select_image', $defaultImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
|
||||||
|
})
|
||||||
|
->selectablePlaceholder(false)
|
||||||
|
->columnSpan(1),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('image')
|
||||||
|
->label('Image')
|
||||||
|
->debounce(500)
|
||||||
|
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||||
|
$egg = Egg::query()->find($get('egg_id'));
|
||||||
|
$images = $egg->docker_images ?? [];
|
||||||
|
|
||||||
|
if (in_array($state, $images)) {
|
||||||
|
$set('select_image', $state);
|
||||||
|
} else {
|
||||||
|
$set('select_image', 'ghcr.io/custom-image');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->placeholder('Enter a custom Image')
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\KeyValue::make('docker_labels')
|
||||||
|
->label('Container Labels')
|
||||||
|
->keyLabel('Label Name')
|
||||||
|
->valueLabel('Label Description')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
Tabs\Tab::make('Egg')
|
||||||
|
->icon('tabler-egg')
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 3,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 5,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('egg_id')
|
||||||
|
->disabledOn('edit')
|
||||||
|
->prefixIcon('tabler-egg')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 3,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 5,
|
||||||
|
])
|
||||||
|
->relationship('egg', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||||
|
->label('Run Egg Install Script?')->inline()
|
||||||
|
->options([
|
||||||
|
false => 'Yes',
|
||||||
|
true => 'Skip',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'primary',
|
||||||
|
true => 'danger',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
false => 'tabler-code',
|
||||||
|
true => 'tabler-code-off',
|
||||||
|
])
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Textarea::make('startup')
|
||||||
|
->label('Startup Command')
|
||||||
|
->required()
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->rows(function ($state) {
|
||||||
|
return str($state)->explode("\n")->reduce(
|
||||||
|
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
Forms\Components\Textarea::make('defaultStartup')
|
||||||
|
->hintAction(CopyAction::make())
|
||||||
|
->label('Default Startup Command')
|
||||||
|
->disabled()
|
||||||
|
->formatStateUsing(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||||
|
$egg = Egg::query()->find($get('egg_id'));
|
||||||
|
|
||||||
|
return $egg->startup;
|
||||||
|
})
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Repeater::make('server_variables')
|
||||||
|
->relationship('serverVariables')
|
||||||
|
->grid()
|
||||||
|
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if (!isset($data['variable_value'])) {
|
||||||
|
$data['variable_value'] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
})
|
||||||
|
->reorderable(false)->addable(false)->deletable(false)
|
||||||
|
->schema(function () {
|
||||||
|
|
||||||
|
$text = Forms\Components\TextInput::make('variable_value')
|
||||||
|
->hidden($this->shouldHideComponent(...))
|
||||||
|
->maxLength(191)
|
||||||
|
->rules([
|
||||||
|
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||||
|
$validator = Validator::make(['validatorkey' => $value], [
|
||||||
|
'validatorkey' => $serverVariable->variable->rules,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||||
|
|
||||||
|
$fail($message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$select = Forms\Components\Select::make('variable_value')
|
||||||
|
->hidden($this->shouldHideComponent(...))
|
||||||
|
->options($this->getSelectOptionsFromRules(...))
|
||||||
|
->selectablePlaceholder(false);
|
||||||
|
|
||||||
|
$components = [$text, $select];
|
||||||
|
|
||||||
|
/** @var Forms\Components\Component $component */
|
||||||
|
foreach ($components as &$component) {
|
||||||
|
$component = $component
|
||||||
|
->live(onBlur: true)
|
||||||
|
->hintIcon('tabler-code')
|
||||||
|
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||||
|
->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules)
|
||||||
|
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||||
|
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $components;
|
||||||
|
})
|
||||||
|
->columnSpan(6),
|
||||||
|
]),
|
||||||
|
Tabs\Tab::make('Mounts')
|
||||||
|
->icon('tabler-layers-linked')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\CheckboxList::make('mounts')
|
||||||
|
->relationship('mounts')
|
||||||
|
->options(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
|
||||||
|
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
|
||||||
|
->label('Mounts')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
Tabs\Tab::make('Databases')
|
||||||
|
->icon('tabler-database')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Placeholder::make('soon')
|
||||||
|
->label('Soon™'),
|
||||||
|
]),
|
||||||
|
Tabs\Tab::make('Actions')
|
||||||
|
->icon('tabler-settings')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Fieldset::make('Server Actions')
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columnSpan(3)
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Actions::make([
|
||||||
|
Forms\Components\Actions\Action::make('toggleInstall')
|
||||||
|
->label('Toggle Install Status')
|
||||||
|
->disabled(fn (Server $server) => $server->isSuspended())
|
||||||
|
->action(function (ServersController $serversController, Server $server) {
|
||||||
|
$serversController->toggleInstall($server);
|
||||||
|
|
||||||
|
return $this->refreshFormData(['status', 'docker']);
|
||||||
|
}),
|
||||||
|
])->fullWidth(),
|
||||||
|
Forms\Components\ToggleButtons::make('')
|
||||||
|
->hint('If you need to change the install status from uninstalled to installed, or vice versa, you may do so with this button.'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columnSpan(3)
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Actions::make([
|
||||||
|
Forms\Components\Actions\Action::make('toggleSuspend')
|
||||||
|
->label('Suspend')
|
||||||
|
->color('warning')
|
||||||
|
->hidden(fn (Server $server) => $server->isSuspended())
|
||||||
|
->action(function (SuspensionService $suspensionService, Server $server) {
|
||||||
|
$suspensionService->toggle($server, 'suspend');
|
||||||
|
Notification::make()->success()->title('Server Suspended!')->send();
|
||||||
|
|
||||||
|
return $this->refreshFormData(['status', 'docker']);
|
||||||
|
}),
|
||||||
|
Forms\Components\Actions\Action::make('toggleUnsuspend')
|
||||||
|
->label('Unsuspend')
|
||||||
|
->color('success')
|
||||||
|
->hidden(fn (Server $server) => !$server->isSuspended())
|
||||||
|
->action(function (SuspensionService $suspensionService, Server $server) {
|
||||||
|
$suspensionService->toggle($server, 'unsuspend');
|
||||||
|
Notification::make()->success()->title('Server Unsuspended!')->send();
|
||||||
|
|
||||||
|
return $this->refreshFormData(['status', 'docker']);
|
||||||
|
}),
|
||||||
|
])->fullWidth(),
|
||||||
|
Forms\Components\ToggleButtons::make('')
|
||||||
|
->hidden(fn (Server $server) => $server->isSuspended())
|
||||||
|
->hint('This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API.'),
|
||||||
|
Forms\Components\ToggleButtons::make('')
|
||||||
|
->hidden(fn (Server $server) => !$server->isSuspended())
|
||||||
|
->hint('This will unsuspend the server and restore normal user access.'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columnSpan(3)
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Actions::make([
|
||||||
|
Forms\Components\Actions\Action::make('transfer')
|
||||||
|
->label('Transfer Soon™')
|
||||||
|
->action(fn (TransferServerService $transfer, Server $server) => $transfer->handle($server, $data))
|
||||||
|
->disabled() //TODO!
|
||||||
|
->form([ //TODO!
|
||||||
|
Forms\Components\Select::make('newNode')
|
||||||
|
->label('New Node')
|
||||||
|
->required()
|
||||||
|
->options([
|
||||||
|
true => 'on',
|
||||||
|
false => 'off',
|
||||||
|
]),
|
||||||
|
Forms\Components\Select::make('newMainAllocation')
|
||||||
|
->label('New Main Allocation')
|
||||||
|
->required()
|
||||||
|
->options([
|
||||||
|
true => 'on',
|
||||||
|
false => 'off',
|
||||||
|
]),
|
||||||
|
Forms\Components\Select::make('newAdditionalAllocation')
|
||||||
|
->label('New Additional Allocations')
|
||||||
|
->options([
|
||||||
|
true => 'on',
|
||||||
|
false => 'off',
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->modalHeading('Transfer'),
|
||||||
|
])->fullWidth(),
|
||||||
|
Forms\Components\ToggleButtons::make('')
|
||||||
|
->hint('Transfer this server to another node connected to this panel. Warning! This feature has not been fully tested and may have bugs.'),
|
||||||
|
]),
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columnSpan(3)
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Actions::make([
|
||||||
|
Forms\Components\Actions\Action::make('reinstall')
|
||||||
|
->label('Reinstall')
|
||||||
|
->color('danger')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading('Are you sure you want to reinstall this server?')
|
||||||
|
->modalDescription('!! This can result in unrecoverable data loss !!')
|
||||||
|
->disabled(fn (Server $server) => $server->isSuspended())
|
||||||
|
->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)),
|
||||||
|
])->fullWidth(),
|
||||||
|
Forms\Components\ToggleButtons::make('')
|
||||||
|
->hint('This will reinstall the server with the assigned egg install script.'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function transferServer(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->columns(2)
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('toNode')
|
||||||
|
->label('New Node'),
|
||||||
|
Forms\Components\TextInput::make('newAllocation')
|
||||||
|
->label('Allocation'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make('Delete')
|
||||||
|
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||||
|
->color('danger')
|
||||||
|
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||||
|
->requiresConfirmation(),
|
||||||
|
Actions\Action::make('console')
|
||||||
|
->label('Console')
|
||||||
|
->icon('tabler-terminal')
|
||||||
|
->url(fn (Server $server) => "/server/$server->uuid_short"),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mutateFormDataBeforeSave(array $data): array
|
||||||
|
{
|
||||||
|
if (!isset($data['description'])) {
|
||||||
|
$data['description'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($data['docker'], $data['status']);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelationManagers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||||
|
{
|
||||||
|
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||||
|
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($component instanceof Forms\Components\Select) {
|
||||||
|
return $containsRuleIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($component instanceof Forms\Components\TextInput) {
|
||||||
|
return !$containsRuleIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception('Component type not supported: ' . $component::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||||
|
{
|
||||||
|
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||||
|
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||||
|
);
|
||||||
|
|
||||||
|
return str($inRule)
|
||||||
|
->after('in:')
|
||||||
|
->explode(',')
|
||||||
|
->each(fn ($value) => str($value)->trim())
|
||||||
|
->mapWithKeys(fn ($value) => [$value => $value])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
588
app/Filament/Resources/ServerResource/Pages/EditServerOrg.php
Normal file
588
app/Filament/Resources/ServerResource/Pages/EditServerOrg.php
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ServerResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ServerResource;
|
||||||
|
use App\Services\Servers\RandomWordService;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Forms;
|
||||||
|
use App\Enums\ContainerStatus;
|
||||||
|
use App\Enums\ServerState;
|
||||||
|
use App\Models\Egg;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\ServerVariable;
|
||||||
|
use App\Repositories\Daemon\DaemonServerRepository;
|
||||||
|
use App\Services\Servers\ServerDeletionService;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class EditServerOrg extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = ServerResource::class;
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('docker')
|
||||||
|
->label('Container Status')->inline()->inlineLabel()
|
||||||
|
->formatStateUsing(function ($state, Server $server) {
|
||||||
|
if ($server->node_id === null) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var DaemonServerRepository $service */
|
||||||
|
$service = resolve(DaemonServerRepository::class);
|
||||||
|
$details = $service->setServer($server)->getDetails();
|
||||||
|
|
||||||
|
return $details['state'] ?? 'unknown';
|
||||||
|
})
|
||||||
|
->options(fn ($state) => collect(ContainerStatus::cases())->filter(fn ($containerStatus) => $containerStatus->value === $state)->mapWithKeys(
|
||||||
|
fn (ContainerStatus $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
|
||||||
|
))
|
||||||
|
->colors(collect(ContainerStatus::cases())->mapWithKeys(
|
||||||
|
fn (ContainerStatus $status) => [$status->value => $status->color()]
|
||||||
|
))
|
||||||
|
->icons(collect(ContainerStatus::cases())->mapWithKeys(
|
||||||
|
fn (ContainerStatus $status) => [$status->value => $status->icon()]
|
||||||
|
))
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('status')
|
||||||
|
->label('Server State')->inline()->inlineLabel()
|
||||||
|
->helperText('')
|
||||||
|
|
||||||
|
->formatStateUsing(fn ($state) => $state ?? ServerState::Normal)
|
||||||
|
->options(fn ($state) => collect(ServerState::cases())->filter(fn ($serverState) => $serverState->value === $state)->mapWithKeys(
|
||||||
|
fn (ServerState $state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]
|
||||||
|
))
|
||||||
|
->colors(collect(ServerState::cases())->mapWithKeys(
|
||||||
|
fn (ServerState $state) => [$state->value => $state->color()]
|
||||||
|
))
|
||||||
|
->icons(collect(ServerState::cases())->mapWithKeys(
|
||||||
|
fn (ServerState $state) => [$state->value => $state->icon()]
|
||||||
|
))
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('external_id')
|
||||||
|
->maxLength(191)
|
||||||
|
->hidden(),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->prefixIcon('tabler-server')
|
||||||
|
->label('Display Name')
|
||||||
|
->suffixAction(Forms\Components\Actions\Action::make('random')
|
||||||
|
->icon('tabler-dice-' . random_int(1, 6))
|
||||||
|
->action(function (Forms\Set $set, Forms\Get $get) {
|
||||||
|
$egg = Egg::find($get('egg_id'));
|
||||||
|
$prefix = $egg ? str($egg->name)->lower()->kebab() . '-' : '';
|
||||||
|
|
||||||
|
$word = (new RandomWordService())->word();
|
||||||
|
|
||||||
|
$set('name', $prefix . $word);
|
||||||
|
}))
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('owner_id')
|
||||||
|
->prefixIcon('tabler-user')
|
||||||
|
->label('Owner')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->relationship('user', 'username')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->hidden()
|
||||||
|
->required()
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('egg_id')
|
||||||
|
->disabledOn('edit')
|
||||||
|
->prefixIcon('tabler-egg')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 2,
|
||||||
|
'lg' => 5,
|
||||||
|
])
|
||||||
|
->relationship('egg', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||||
|
->label('Run Egg Install Script?')->inline()
|
||||||
|
->options([
|
||||||
|
false => 'Yes',
|
||||||
|
true => 'Skip',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'primary',
|
||||||
|
true => 'danger',
|
||||||
|
])
|
||||||
|
->icons([
|
||||||
|
false => 'tabler-code',
|
||||||
|
true => 'tabler-code-off',
|
||||||
|
])
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Forms\Components\Textarea::make('startup')
|
||||||
|
->hintIcon('tabler-code')
|
||||||
|
->label('Startup Command')
|
||||||
|
->required()
|
||||||
|
->live()
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->rows(function ($state) {
|
||||||
|
return str($state)->explode("\n")->reduce(
|
||||||
|
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('start_on_completion'),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Egg Variables')
|
||||||
|
->icon('tabler-eggs')
|
||||||
|
->iconColor('primary')
|
||||||
|
->collapsible()
|
||||||
|
->collapsed()
|
||||||
|
->columnSpan(([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
]))
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Repeater::make('server_variables')
|
||||||
|
->relationship('serverVariables')
|
||||||
|
->grid()
|
||||||
|
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
if (!isset($data['variable_value'])) {
|
||||||
|
$data['variable_value'] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
})
|
||||||
|
->reorderable(false)->addable(false)->deletable(false)
|
||||||
|
->schema(function () {
|
||||||
|
|
||||||
|
$text = Forms\Components\TextInput::make('variable_value')
|
||||||
|
->hidden($this->shouldHideComponent(...))
|
||||||
|
->maxLength(191)
|
||||||
|
->rules([
|
||||||
|
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||||
|
$validator = Validator::make(['validatorkey' => $value], [
|
||||||
|
'validatorkey' => $serverVariable->variable->rules,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||||
|
|
||||||
|
$fail($message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$select = Forms\Components\Select::make('variable_value')
|
||||||
|
->hidden($this->shouldHideComponent(...))
|
||||||
|
->options($this->getSelectOptionsFromRules(...))
|
||||||
|
->selectablePlaceholder(false);
|
||||||
|
|
||||||
|
$components = [$text, $select];
|
||||||
|
|
||||||
|
/** @var Forms\Components\Component $component */
|
||||||
|
foreach ($components as &$component) {
|
||||||
|
$component = $component
|
||||||
|
->live(onBlur: true)
|
||||||
|
->hintIcon('tabler-code')
|
||||||
|
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||||
|
->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules)
|
||||||
|
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||||
|
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $components;
|
||||||
|
})
|
||||||
|
->columnSpan(2),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Environment Management')
|
||||||
|
->collapsed()
|
||||||
|
->icon('tabler-server-cog')
|
||||||
|
->iconColor('primary')
|
||||||
|
->columns([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 4,
|
||||||
|
])
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Fieldset::make('Resource Limits')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_mem')
|
||||||
|
->label('Memory')->inlineLabel()->inline()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('memory', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('memory') == 0)
|
||||||
|
->live()
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('memory')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_mem'))
|
||||||
|
->label('Memory Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_disk')
|
||||||
|
->label('Disk Space')->inlineLabel()->inline()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('disk', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('disk') == 0)
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('disk')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_disk'))
|
||||||
|
->label('Disk Space Limit')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('unlimited_cpu')
|
||||||
|
->label('CPU')->inlineLabel()->inline()
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set) => $set('cpu', 0))
|
||||||
|
->formatStateUsing(fn (Forms\Get $get) => $get('cpu') == 0)
|
||||||
|
->live()
|
||||||
|
->options([
|
||||||
|
true => 'Unlimited',
|
||||||
|
false => 'Limited',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
true => 'primary',
|
||||||
|
false => 'warning',
|
||||||
|
])
|
||||||
|
->columnSpan(2),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('cpu')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => $get('unlimited_cpu'))
|
||||||
|
->label('CPU Limit')->inlineLabel()
|
||||||
|
->suffix('%')
|
||||||
|
->required()
|
||||||
|
->columnSpan(2)
|
||||||
|
->numeric()
|
||||||
|
->minValue(0),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('swap_support')
|
||||||
|
->live()
|
||||||
|
->label('Enable Swap Memory')->inlineLabel()->inline()
|
||||||
|
->columnSpan(2)
|
||||||
|
->afterStateUpdated(function ($state, Forms\Set $set) {
|
||||||
|
$value = match ($state) {
|
||||||
|
'unlimited' => -1,
|
||||||
|
'disabled' => 0,
|
||||||
|
'limited' => 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
$set('swap', $value);
|
||||||
|
})
|
||||||
|
->formatStateUsing(function (Forms\Get $get) {
|
||||||
|
return match (true) {
|
||||||
|
$get('swap') > 0 => 'limited',
|
||||||
|
$get('swap') == 0 => 'disabled',
|
||||||
|
$get('swap') < 0 => 'unlimited',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
->options([
|
||||||
|
'unlimited' => 'Unlimited',
|
||||||
|
'limited' => 'Limited',
|
||||||
|
'disabled' => 'Disabled',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
'unlimited' => 'primary',
|
||||||
|
'limited' => 'warning',
|
||||||
|
'disabled' => 'danger',
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('swap')
|
||||||
|
->dehydratedWhenHidden()
|
||||||
|
->hidden(fn (Forms\Get $get) => match ($get('swap_support')) {
|
||||||
|
'disabled', 'unlimited', true => true,
|
||||||
|
'limited', false => false,
|
||||||
|
})
|
||||||
|
->label('Swap Memory')->inlineLabel()
|
||||||
|
->suffix('MiB')
|
||||||
|
->minValue(-1)
|
||||||
|
->columnSpan(2)
|
||||||
|
->required()
|
||||||
|
->integer(),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('io')
|
||||||
|
->helperText('The IO performance relative to other running containers')
|
||||||
|
->label('Block IO Proportion'),
|
||||||
|
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->columns(4)
|
||||||
|
->columnSpanFull()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\ToggleButtons::make('oom_killer')
|
||||||
|
->label('OOM Killer')->inlineLabel()->inline()
|
||||||
|
->columnSpan(2)
|
||||||
|
->options([
|
||||||
|
false => 'Disabled',
|
||||||
|
true => 'Enabled',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'success',
|
||||||
|
true => 'danger',
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('oom_disabled_hidden')
|
||||||
|
->hidden(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Fieldset::make('Feature Limits')
|
||||||
|
->inlineLabel()
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('allocation_limit')
|
||||||
|
->suffixIcon('tabler-network')
|
||||||
|
->required()
|
||||||
|
->numeric(),
|
||||||
|
Forms\Components\TextInput::make('database_limit')
|
||||||
|
->suffixIcon('tabler-database')
|
||||||
|
->required()
|
||||||
|
->numeric(),
|
||||||
|
Forms\Components\TextInput::make('backup_limit')
|
||||||
|
->suffixIcon('tabler-copy-check')
|
||||||
|
->required()
|
||||||
|
->numeric(),
|
||||||
|
]),
|
||||||
|
Forms\Components\Fieldset::make('Docker Settings')
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 2,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 2,
|
||||||
|
'md' => 3,
|
||||||
|
'lg' => 3,
|
||||||
|
])
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('select_image')
|
||||||
|
->label('Image Name')
|
||||||
|
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('image', $state))
|
||||||
|
->options(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||||
|
$egg = Egg::query()->find($get('egg_id'));
|
||||||
|
$images = $egg->docker_images ?? [];
|
||||||
|
|
||||||
|
$currentImage = $get('image');
|
||||||
|
if (!$currentImage && $images) {
|
||||||
|
$defaultImage = collect($images)->first();
|
||||||
|
$set('image', $defaultImage);
|
||||||
|
$set('select_image', $defaultImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
|
||||||
|
})
|
||||||
|
->selectablePlaceholder(false)
|
||||||
|
->columnSpan(1),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('image')
|
||||||
|
->label('Image')
|
||||||
|
->debounce(500)
|
||||||
|
->afterStateUpdated(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||||
|
$egg = Egg::query()->find($get('egg_id'));
|
||||||
|
$images = $egg->docker_images ?? [];
|
||||||
|
|
||||||
|
if (in_array($state, $images)) {
|
||||||
|
$set('select_image', $state);
|
||||||
|
} else {
|
||||||
|
$set('select_image', 'ghcr.io/custom-image');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->placeholder('Enter a custom Image')
|
||||||
|
->columnSpan(1),
|
||||||
|
|
||||||
|
Forms\Components\KeyValue::make('docker_labels')
|
||||||
|
->label('Container Labels')
|
||||||
|
->keyLabel('Label Name')
|
||||||
|
->valueLabel('Label Description')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make('Delete')
|
||||||
|
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||||
|
->color('danger')
|
||||||
|
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||||
|
->requiresConfirmation(),
|
||||||
|
Actions\Action::make('console')
|
||||||
|
->label('Console')
|
||||||
|
->icon('tabler-terminal')
|
||||||
|
->url(fn (Server $server) => "/server/$server->uuid_short"),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mutateFormDataBeforeSave(array $data): array
|
||||||
|
{
|
||||||
|
unset($data['docker'], $data['status']);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRelationManagers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||||
|
{
|
||||||
|
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||||
|
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($component instanceof Forms\Components\Select) {
|
||||||
|
return $containsRuleIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($component instanceof Forms\Components\TextInput) {
|
||||||
|
return !$containsRuleIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception('Component type not supported: ' . $component::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||||
|
{
|
||||||
|
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||||
|
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||||
|
);
|
||||||
|
|
||||||
|
return str($inRule)
|
||||||
|
->after('in:')
|
||||||
|
->explode(',')
|
||||||
|
->each(fn ($value) => str($value)->trim())
|
||||||
|
->mapWithKeys(fn ($value) => [$value => $value])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
112
app/Filament/Resources/ServerResource/Pages/ListServers.php
Normal file
112
app/Filament/Resources/ServerResource/Pages/ListServers.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ServerResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ServerResource;
|
||||||
|
use App\Models\Server;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Actions\CreateAction;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables;
|
||||||
|
|
||||||
|
class ListServers extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = ServerResource::class;
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('status')
|
||||||
|
->default('unknown')
|
||||||
|
->badge()
|
||||||
|
->default(function (Server $server) {
|
||||||
|
if ($server->status !== null) {
|
||||||
|
return $server->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $server->retrieveStatus() ?? 'node_fail';
|
||||||
|
})
|
||||||
|
->icon(fn ($state) => match ($state) {
|
||||||
|
'node_fail' => 'tabler-server-off',
|
||||||
|
'running' => 'tabler-heartbeat',
|
||||||
|
'removing' => 'tabler-heart-x',
|
||||||
|
'offline' => 'tabler-heart-off',
|
||||||
|
'paused' => 'tabler-heart-pause',
|
||||||
|
'installing' => 'tabler-heart-bolt',
|
||||||
|
'suspended' => 'tabler-heart-cancel',
|
||||||
|
default => 'tabler-heart-question',
|
||||||
|
})
|
||||||
|
->color(fn ($state): string => match ($state) {
|
||||||
|
'running' => 'success',
|
||||||
|
'installing', 'restarting' => 'primary',
|
||||||
|
'paused', 'removing' => 'warning',
|
||||||
|
'node_fail', 'install_failed', 'suspended' => 'danger',
|
||||||
|
default => 'gray',
|
||||||
|
}),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('uuid')
|
||||||
|
->hidden()
|
||||||
|
->label('UUID')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->icon('tabler-brand-docker')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('node.name')
|
||||||
|
->icon('tabler-server-2')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('egg.name')
|
||||||
|
->icon('tabler-egg')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('user.username')
|
||||||
|
->icon('tabler-user')
|
||||||
|
->label('Owner')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\SelectColumn::make('allocation_id')
|
||||||
|
->label('Primary Allocation')
|
||||||
|
->options(fn ($state, Server $server) => $server->allocations->mapWithKeys(
|
||||||
|
fn ($allocation) => [$allocation->id => $allocation->address])
|
||||||
|
)
|
||||||
|
->selectablePlaceholder(false)
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('image')->hidden(),
|
||||||
|
Tables\Columns\TextColumn::make('backups_count')
|
||||||
|
->counts('backups')
|
||||||
|
->label('Backups')
|
||||||
|
->icon('tabler-file-download')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\Action::make('View')
|
||||||
|
->icon('tabler-terminal')
|
||||||
|
->url(fn (Server $server) => "/server/$server->uuid_short"),
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->emptyStateIcon('tabler-brand-docker')
|
||||||
|
->emptyStateDescription('')
|
||||||
|
->emptyStateHeading('No Servers')
|
||||||
|
->emptyStateActions([
|
||||||
|
CreateAction::make('create')
|
||||||
|
->label('Create Server')
|
||||||
|
->button(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make()
|
||||||
|
->label('Create Server')
|
||||||
|
->hidden(fn () => Server::count() <= 0),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ServerResource\RelationManagers;
|
||||||
|
|
||||||
|
use App\Models\Allocation;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class AllocationsRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'allocations';
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('ip')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->recordTitleAttribute('ip')
|
||||||
|
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
|
||||||
|
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
|
||||||
|
// ->actions
|
||||||
|
// ->groups
|
||||||
|
->inverseRelationship('server')
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('ip')->label('IP'),
|
||||||
|
Tables\Columns\TextColumn::make('port')->label('Port'),
|
||||||
|
Tables\Columns\TextInputColumn::make('ip_alias')->label('Alias'),
|
||||||
|
Tables\Columns\IconColumn::make('primary')
|
||||||
|
->icon(fn ($state) => match ($state) {
|
||||||
|
false => 'tabler-star',
|
||||||
|
true => 'tabler-star-filled',
|
||||||
|
})
|
||||||
|
->color(fn ($state) => match ($state) {
|
||||||
|
false => 'gray',
|
||||||
|
true => 'warning',
|
||||||
|
})
|
||||||
|
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
|
||||||
|
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
|
||||||
|
->label('Primary'),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\Action::make('make-primary')
|
||||||
|
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
|
||||||
|
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||||
|
])
|
||||||
|
->headerActions([
|
||||||
|
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||||
|
Tables\Actions\AssociateAction::make()
|
||||||
|
->multiple()
|
||||||
|
->preloadRecordSelect()
|
||||||
|
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
|
||||||
|
->label('Add Allocation'),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DissociateBulkAction::make(),
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Filament/Resources/UserResource.php
Normal file
37
app/Filament/Resources/UserResource.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\UserResource\Pages;
|
||||||
|
use App\Filament\Resources\UserResource\RelationManagers;
|
||||||
|
use App\Models\User;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
|
||||||
|
class UserResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = User::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'tabler-users';
|
||||||
|
|
||||||
|
protected static ?string $recordTitleAttribute = 'username';
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::count() ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
RelationManagers\ServersRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListUsers::route('/'),
|
||||||
|
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
306
app/Filament/Resources/UserResource/Pages/EditProfile.php
Normal file
306
app/Filament/Resources/UserResource/Pages/EditProfile.php
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
|
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
|
||||||
|
use App\Facades\Activity;
|
||||||
|
use App\Models\ActivityLog;
|
||||||
|
use App\Models\ApiKey;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Users\ToggleTwoFactorService;
|
||||||
|
use App\Services\Users\TwoFactorSetupService;
|
||||||
|
use chillerlan\QRCode\Common\EccLevel;
|
||||||
|
use chillerlan\QRCode\Common\Version;
|
||||||
|
use chillerlan\QRCode\QRCode;
|
||||||
|
use chillerlan\QRCode\QROptions;
|
||||||
|
use Filament\Forms\Components\Actions\Action;
|
||||||
|
use Filament\Forms\Components\Grid;
|
||||||
|
use Filament\Forms\Components\Placeholder;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\Tabs;
|
||||||
|
use Filament\Forms\Components\TagsInput;
|
||||||
|
use Filament\Forms\Components\Tabs\Tab;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Get;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\HtmlString;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||||
|
{
|
||||||
|
protected function getForms(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'form' => $this->form(
|
||||||
|
$this->makeForm()
|
||||||
|
->schema([
|
||||||
|
Tabs::make()->persistTabInQueryString()
|
||||||
|
->schema([
|
||||||
|
Tab::make('Account')
|
||||||
|
->label(trans('strings.account'))
|
||||||
|
->icon('tabler-user')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('username')
|
||||||
|
->label(trans('strings.username'))
|
||||||
|
->disabled()
|
||||||
|
->readOnly()
|
||||||
|
->maxLength(191)
|
||||||
|
->unique(ignoreRecord: true)
|
||||||
|
->autofocus(),
|
||||||
|
|
||||||
|
TextInput::make('email')
|
||||||
|
->prefixIcon('tabler-mail')
|
||||||
|
->label(trans('strings.email'))
|
||||||
|
->email()
|
||||||
|
->required()
|
||||||
|
->maxLength(191)
|
||||||
|
->unique(ignoreRecord: true),
|
||||||
|
|
||||||
|
TextInput::make('password')
|
||||||
|
->label(trans('strings.password'))
|
||||||
|
->password()
|
||||||
|
->prefixIcon('tabler-password')
|
||||||
|
->revealable(filament()->arePasswordsRevealable())
|
||||||
|
->rule(Password::default())
|
||||||
|
->autocomplete('new-password')
|
||||||
|
->dehydrated(fn ($state): bool => filled($state))
|
||||||
|
->dehydrateStateUsing(fn ($state): string => Hash::make($state))
|
||||||
|
->live(debounce: 500)
|
||||||
|
->same('passwordConfirmation'),
|
||||||
|
|
||||||
|
TextInput::make('passwordConfirmation')
|
||||||
|
->label(trans('strings.password_confirmation'))
|
||||||
|
->password()
|
||||||
|
->prefixIcon('tabler-password-fingerprint')
|
||||||
|
->revealable(filament()->arePasswordsRevealable())
|
||||||
|
->required()
|
||||||
|
->visible(fn (Get $get): bool => filled($get('password')))
|
||||||
|
->dehydrated(false),
|
||||||
|
|
||||||
|
Select::make('language')
|
||||||
|
->label(trans('strings.language'))
|
||||||
|
->required()
|
||||||
|
->prefixIcon('tabler-flag')
|
||||||
|
->live()
|
||||||
|
->default('en')
|
||||||
|
->helperText(fn (User $user, $state) => new HtmlString($user->isLanguageTranslated($state) ? '' : "
|
||||||
|
Your language ($state) has not been translated yet!
|
||||||
|
But never fear, you can help fix that by
|
||||||
|
<a style='color: rgb(56, 189, 248)' href='https://crowdin.com/project/pelican-dev'>contributing directly here</a>.
|
||||||
|
")
|
||||||
|
)
|
||||||
|
->options(fn (User $user) => $user->getAvailableLanguages()),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Tab::make('2FA')
|
||||||
|
->icon('tabler-shield-lock')
|
||||||
|
->schema(function () {
|
||||||
|
|
||||||
|
if ($this->getUser()->use_totp) {
|
||||||
|
return [
|
||||||
|
Placeholder::make('2fa-already-enabled')
|
||||||
|
->label('Two Factor Authentication is currently enabled!'),
|
||||||
|
Textarea::make('backup-tokens')
|
||||||
|
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||||
|
->rows(10)
|
||||||
|
->readOnly()
|
||||||
|
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||||
|
->helperText('These will not be shown again!')
|
||||||
|
->label('Backup Tokens:'),
|
||||||
|
TextInput::make('2fa-disable-code')
|
||||||
|
->label('Disable 2FA')
|
||||||
|
->helperText('Enter your current 2FA code to disable Two Factor Authentication'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$setupService = app(TwoFactorSetupService::class);
|
||||||
|
|
||||||
|
['image_url_data' => $url, 'secret' => $secret] = cache()->remember(
|
||||||
|
"users.{$this->getUser()->id}.2fa.state",
|
||||||
|
now()->addMinutes(5), fn () => $setupService->handle($this->getUser())
|
||||||
|
);
|
||||||
|
|
||||||
|
$options = new QROptions([
|
||||||
|
'svgLogo' => public_path('pelican.svg'),
|
||||||
|
'addLogoSpace' => true,
|
||||||
|
'logoSpaceWidth' => 13,
|
||||||
|
'logoSpaceHeight' => 13,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// https://github.com/chillerlan/php-qrcode/blob/main/examples/svgWithLogo.php
|
||||||
|
|
||||||
|
// SVG logo options (see extended class)
|
||||||
|
$options->svgLogo = public_path('pelican.svg'); // logo from: https://github.com/simple-icons/simple-icons
|
||||||
|
$options->svgLogoScale = 0.05;
|
||||||
|
// $options->svgLogoCssClass = 'dark';
|
||||||
|
|
||||||
|
// QROptions
|
||||||
|
$options->version = Version::AUTO;
|
||||||
|
// $options->outputInterface = QRSvgWithLogo::class;
|
||||||
|
$options->outputBase64 = false;
|
||||||
|
$options->eccLevel = EccLevel::H; // ECC level H is necessary when using logos
|
||||||
|
$options->addQuietzone = true;
|
||||||
|
// $options->drawLightModules = true;
|
||||||
|
$options->connectPaths = true;
|
||||||
|
$options->drawCircularModules = true;
|
||||||
|
// $options->circleRadius = 0.45;
|
||||||
|
|
||||||
|
$options->svgDefs = '<linearGradient id="gradient" x1="100%" y2="100%">
|
||||||
|
<stop stop-color="#7dd4fc" offset="0"/>
|
||||||
|
<stop stop-color="#38bdf8" offset="0.5"/>
|
||||||
|
<stop stop-color="#0369a1" offset="1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<style><![CDATA[
|
||||||
|
.dark{fill: url(#gradient);}
|
||||||
|
.light{fill: #000;}
|
||||||
|
]]></style>';
|
||||||
|
|
||||||
|
$image = (new QRCode($options))->render($url);
|
||||||
|
|
||||||
|
return [
|
||||||
|
Placeholder::make('qr')
|
||||||
|
->label('Scan QR Code')
|
||||||
|
->content(fn () => new HtmlString("
|
||||||
|
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
|
||||||
|
"))
|
||||||
|
->helperText('Setup Key: '. $secret),
|
||||||
|
TextInput::make('2facode')
|
||||||
|
->label('Code')
|
||||||
|
->requiredWith('2fapassword')
|
||||||
|
->helperText('Scan the QR code above using your two-step authentication app, then enter the code generated.'),
|
||||||
|
TextInput::make('2fapassword')
|
||||||
|
->label('Current Password')
|
||||||
|
->requiredWith('2facode')
|
||||||
|
->currentPassword()
|
||||||
|
->password()
|
||||||
|
->helperText('Enter your current password to verify.'),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
|
||||||
|
Tab::make('API Keys')
|
||||||
|
->icon('tabler-key')
|
||||||
|
->schema([
|
||||||
|
Grid::make('asdf')->columns(5)->schema([
|
||||||
|
Section::make('Create API Key')->columnSpan(3)->schema([
|
||||||
|
TextInput::make('description')->required(),
|
||||||
|
TagsInput::make('allowed_ips')
|
||||||
|
->splitKeys([',', ' ', 'Tab'])
|
||||||
|
->placeholder('Example: 127.0.0.1 or 192.168.1.1')
|
||||||
|
->label('Whitelisted IP\'s')
|
||||||
|
->helperText('Press enter to add a new IP address or leave blank to allow any IP address')
|
||||||
|
->columnSpanFull(),
|
||||||
|
])->headerActions([
|
||||||
|
Action::make('Create')
|
||||||
|
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
|
||||||
|
->action(function (Get $get, Action $action) {
|
||||||
|
$token = auth()->user()->createToken(
|
||||||
|
$get('description'),
|
||||||
|
$get('allowed_ips'),
|
||||||
|
);
|
||||||
|
|
||||||
|
Activity::event('user:api-key.create')
|
||||||
|
->subject($token->accessToken)
|
||||||
|
->property('identifier', $token->accessToken->identifier)
|
||||||
|
->log();
|
||||||
|
|
||||||
|
$action->success();
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
Section::make('Keys')->columnSpan(2)->schema([
|
||||||
|
Repeater::make('keys')
|
||||||
|
->label('')
|
||||||
|
->relationship('apiKeys')
|
||||||
|
->addable(false)
|
||||||
|
->itemLabel(fn ($state) => $state['identifier'])
|
||||||
|
->deleteAction(function (Action $action) {
|
||||||
|
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component) {
|
||||||
|
$items = $component->getState();
|
||||||
|
$key = $items[$arguments['item']];
|
||||||
|
ApiKey::find($key['id'] ?? null)?->delete();
|
||||||
|
|
||||||
|
unset($items[$arguments['item']]);
|
||||||
|
|
||||||
|
$component->state($items);
|
||||||
|
|
||||||
|
$component->callAfterStateUpdated();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->schema(fn () => [
|
||||||
|
Placeholder::make('adf')->label(fn (ApiKey $key) => $key->memo),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Tab::make('SSH Keys')
|
||||||
|
->icon('tabler-lock-code')
|
||||||
|
->schema([
|
||||||
|
Placeholder::make('Coming soon!'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Tab::make('Activity')
|
||||||
|
->icon('tabler-history')
|
||||||
|
->schema([
|
||||||
|
Repeater::make('activity')
|
||||||
|
->deletable(false)
|
||||||
|
->addable(false)
|
||||||
|
->relationship(null, function (Builder $query) {
|
||||||
|
$query->orderBy('timestamp', 'desc');
|
||||||
|
})
|
||||||
|
->schema([
|
||||||
|
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->operation('edit')
|
||||||
|
->model($this->getUser())
|
||||||
|
->statePath('data')
|
||||||
|
->inlineLabel(!static::isSimple()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function handleRecordUpdate($record, $data): \Illuminate\Database\Eloquent\Model
|
||||||
|
{
|
||||||
|
if ($token = $data['2facode'] ?? null) {
|
||||||
|
/** @var ToggleTwoFactorService $service */
|
||||||
|
$service = resolve(ToggleTwoFactorService::class);
|
||||||
|
|
||||||
|
$tokens = $service->handle($record, $token, true);
|
||||||
|
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
|
||||||
|
|
||||||
|
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token = $data['2fa-disable-code'] ?? null) {
|
||||||
|
/** @var ToggleTwoFactorService $service */
|
||||||
|
$service = resolve(ToggleTwoFactorService::class);
|
||||||
|
|
||||||
|
$service->handle($record, $token, false);
|
||||||
|
|
||||||
|
cache()->forget("users.$record->id.2fa.state");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::handleRecordUpdate($record, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exception($e, $stopPropagation): void
|
||||||
|
{
|
||||||
|
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Invalid 2FA Code')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->color('danger')
|
||||||
|
->icon('tabler-2fa')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
$stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
app/Filament/Resources/UserResource/Pages/EditUser.php
Normal file
78
app/Filament/Resources/UserResource/Pages/EditUser.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\UserResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
use App\Models\User;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Section;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class EditUser extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = UserResource::class;
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Section::make()->schema([
|
||||||
|
Forms\Components\TextInput::make('username')->required()->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('email')->email()->required()->maxLength(191),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('password')
|
||||||
|
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||||
|
->dehydrated(fn (?string $state): bool => filled($state))
|
||||||
|
->required(fn (string $operation): bool => $operation === 'create')
|
||||||
|
->password(),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('root_admin')
|
||||||
|
->label('Administrator (Root)')
|
||||||
|
->options([
|
||||||
|
false => 'No',
|
||||||
|
true => 'Admin',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'primary',
|
||||||
|
true => 'danger',
|
||||||
|
])
|
||||||
|
->disableOptionWhen(function (string $operation, $value, User $user) {
|
||||||
|
if ($operation !== 'edit' || $value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->isLastRootAdmin();
|
||||||
|
})
|
||||||
|
->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '')
|
||||||
|
->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '')
|
||||||
|
->hintColor('warning')
|
||||||
|
->inline()
|
||||||
|
->required()
|
||||||
|
->default(false),
|
||||||
|
|
||||||
|
Forms\Components\Hidden::make('skipValidation')->default(true),
|
||||||
|
|
||||||
|
Forms\Components\Select::make('language')
|
||||||
|
->required()
|
||||||
|
->hidden()
|
||||||
|
->default('en')
|
||||||
|
->options(fn (User $user) => $user->getAvailableLanguages()),
|
||||||
|
|
||||||
|
])->columns(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
$this->getSaveFormAction()->formId('form'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
124
app/Filament/Resources/UserResource/Pages/ListUsers.php
Normal file
124
app/Filament/Resources/UserResource/Pages/ListUsers.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\UserResource;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Users\UserCreationService;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Forms;
|
||||||
|
|
||||||
|
class ListUsers extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = UserResource::class;
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\ImageColumn::make('picture')
|
||||||
|
->visibleFrom('lg')
|
||||||
|
->label('')
|
||||||
|
->extraImgAttributes(['class' => 'rounded-full'])
|
||||||
|
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||||
|
Tables\Columns\TextColumn::make('external_id')
|
||||||
|
->searchable()
|
||||||
|
->hidden(),
|
||||||
|
Tables\Columns\TextColumn::make('uuid')
|
||||||
|
->label('UUID')
|
||||||
|
->hidden()
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('username')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('email')
|
||||||
|
->searchable()
|
||||||
|
->icon('tabler-mail'),
|
||||||
|
Tables\Columns\IconColumn::make('root_admin')
|
||||||
|
->visibleFrom('md')
|
||||||
|
->label('Admin')
|
||||||
|
->boolean()
|
||||||
|
->trueIcon('tabler-star-filled')
|
||||||
|
->falseIcon('tabler-star-off')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\IconColumn::make('use_totp')->label('2FA')
|
||||||
|
->visibleFrom('lg')
|
||||||
|
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
||||||
|
->boolean()->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('servers_count')
|
||||||
|
->counts('servers')
|
||||||
|
->icon('tabler-server')
|
||||||
|
->label('Servers'),
|
||||||
|
Tables\Columns\TextColumn::make('subusers_count')
|
||||||
|
->visibleFrom('sm')
|
||||||
|
->label('Subusers')
|
||||||
|
->counts('subusers')
|
||||||
|
->icon('tabler-users'),
|
||||||
|
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->checkIfRecordIsSelectableUsing(fn (User $user) => !$user->servers_count)
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make('create')
|
||||||
|
->label('Create User')
|
||||||
|
->createAnother(false)
|
||||||
|
->form([
|
||||||
|
Forms\Components\Grid::make()
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('username')
|
||||||
|
->alphaNum()
|
||||||
|
->required()
|
||||||
|
->maxLength(191),
|
||||||
|
Forms\Components\TextInput::make('email')
|
||||||
|
->email()
|
||||||
|
->required()
|
||||||
|
->unique()
|
||||||
|
->maxLength(191),
|
||||||
|
|
||||||
|
Forms\Components\TextInput::make('password')
|
||||||
|
->hintIcon('tabler-question-mark')
|
||||||
|
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||||
|
->password(),
|
||||||
|
|
||||||
|
Forms\Components\ToggleButtons::make('root_admin')
|
||||||
|
->label('Administrator (Root)')
|
||||||
|
->options([
|
||||||
|
false => 'No',
|
||||||
|
true => 'Admin',
|
||||||
|
])
|
||||||
|
->colors([
|
||||||
|
false => 'primary',
|
||||||
|
true => 'danger',
|
||||||
|
])
|
||||||
|
->inline()
|
||||||
|
->required()
|
||||||
|
->default(false),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
||||||
|
->action(function (array $data) {
|
||||||
|
resolve(UserCreationService::class)->handle($data);
|
||||||
|
Notification::make()->title('User Created!')->success()->send();
|
||||||
|
|
||||||
|
return redirect()->route('filament.admin.resources.users.index');
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\UserResource\RelationManagers;
|
||||||
|
|
||||||
|
use App\Enums\ServerState;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Servers\SuspensionService;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables\Actions;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
|
||||||
|
class ServersRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'servers';
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->getOwnerRecord();
|
||||||
|
|
||||||
|
return $table
|
||||||
|
->searchable(false)
|
||||||
|
->headerActions([
|
||||||
|
Actions\Action::make('toggleSuspend')
|
||||||
|
->hidden(fn () => $user->servers()
|
||||||
|
->whereNot('status', ServerState::Suspended)
|
||||||
|
->orWhereNull('status')
|
||||||
|
->count() === 0
|
||||||
|
)
|
||||||
|
->label('Suspend All Servers')
|
||||||
|
->color('warning')
|
||||||
|
->action(function () use ($user) {
|
||||||
|
foreach ($user->servers()->whereNot('status', ServerState::Suspended)->get() as $server) {
|
||||||
|
resolve(SuspensionService::class)->toggle($server);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Actions\Action::make('toggleUnsuspend')
|
||||||
|
->hidden(fn () => $user->servers()->where('status', ServerState::Suspended)->count() === 0)
|
||||||
|
->label('Unsuspend All Servers')
|
||||||
|
->color('primary')
|
||||||
|
->action(function () use ($user) {
|
||||||
|
foreach ($user->servers()->where('status', ServerState::Suspended)->get() as $server) {
|
||||||
|
resolve(SuspensionService::class)->toggle($server, SuspensionService::ACTION_UNSUSPEND);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('uuid')
|
||||||
|
->hidden()
|
||||||
|
->label('UUID')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->icon('tabler-brand-docker')
|
||||||
|
->label(trans('strings.name'))
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('node.name')
|
||||||
|
->icon('tabler-server-2')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node]))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('egg.name')
|
||||||
|
->icon('tabler-egg')
|
||||||
|
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\SelectColumn::make('allocation.id')
|
||||||
|
->label('Primary Allocation')
|
||||||
|
->options(fn ($state, Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||||
|
->selectablePlaceholder(false)
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('image')->hidden(),
|
||||||
|
Tables\Columns\TextColumn::make('databases_count')
|
||||||
|
->counts('databases')
|
||||||
|
->label('Databases')
|
||||||
|
->icon('tabler-database')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('backups_count')
|
||||||
|
->counts('backups')
|
||||||
|
->label('Backups')
|
||||||
|
->icon('tabler-file-download')
|
||||||
|
->numeric()
|
||||||
|
->sortable(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ class EggController extends Controller
|
|||||||
{
|
{
|
||||||
$data = $request->validated();
|
$data = $request->validated();
|
||||||
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
|
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
|
||||||
|
$data['author'] = $request->user()->email;
|
||||||
|
|
||||||
$egg = $this->creationService->handle($data);
|
$egg = $this->creationService->handle($data);
|
||||||
$this->alert->success(trans('admin/eggs.notices.egg_created'))->flash();
|
$this->alert->success(trans('admin/eggs.notices.egg_created'))->flash();
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
|
||||||
|
|
||||||
use App\Models\Egg;
|
|
||||||
use Ramsey\Uuid\Uuid;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use App\Models\Mount;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Prologue\Alerts\AlertsMessageBag;
|
|
||||||
use Illuminate\View\Factory as ViewFactory;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Http\Requests\Admin\MountFormRequest;
|
|
||||||
|
|
||||||
class MountController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* MountController constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
protected AlertsMessageBag $alert,
|
|
||||||
protected ViewFactory $view
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the mount overview page.
|
|
||||||
*/
|
|
||||||
public function index(): View
|
|
||||||
{
|
|
||||||
return view('admin.mounts.index', [
|
|
||||||
'mounts' => Mount::query()->withCount(['eggs', 'nodes'])->get(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the mount view page.
|
|
||||||
*/
|
|
||||||
public function view(string $id): View
|
|
||||||
{
|
|
||||||
return view('admin.mounts.view', [
|
|
||||||
'mount' => Mount::with(['eggs', 'nodes'])->findOrFail($id),
|
|
||||||
'eggs' => Egg::all(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle request to create new mount.
|
|
||||||
*
|
|
||||||
* @throws \Throwable
|
|
||||||
*/
|
|
||||||
public function create(MountFormRequest $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$model = (new Mount())->fill($request->validated());
|
|
||||||
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
|
|
||||||
|
|
||||||
$model->saveOrFail();
|
|
||||||
$mount = $model->fresh();
|
|
||||||
|
|
||||||
$this->alert->success('Mount was created successfully.')->flash();
|
|
||||||
|
|
||||||
return redirect()->route('admin.mounts.view', $mount->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle request to update or delete location.
|
|
||||||
*
|
|
||||||
* @throws \Throwable
|
|
||||||
*/
|
|
||||||
public function update(MountFormRequest $request, Mount $mount): RedirectResponse
|
|
||||||
{
|
|
||||||
if ($request->input('action') === 'delete') {
|
|
||||||
return $this->delete($mount);
|
|
||||||
}
|
|
||||||
|
|
||||||
$mount->forceFill($request->validated())->save();
|
|
||||||
|
|
||||||
$this->alert->success('Mount was updated successfully.')->flash();
|
|
||||||
|
|
||||||
return redirect()->route('admin.mounts.view', $mount->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a location from the system.
|
|
||||||
*
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function delete(Mount $mount): RedirectResponse
|
|
||||||
{
|
|
||||||
$mount->delete();
|
|
||||||
|
|
||||||
return redirect()->route('admin.mounts');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds eggs to the mount's many-to-many relation.
|
|
||||||
*/
|
|
||||||
public function addEggs(Request $request, Mount $mount): RedirectResponse
|
|
||||||
{
|
|
||||||
$validatedData = $request->validate([
|
|
||||||
'eggs' => 'required|exists:eggs,id',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$eggs = $validatedData['eggs'] ?? [];
|
|
||||||
if (count($eggs) > 0) {
|
|
||||||
$mount->eggs()->attach($eggs);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->alert->success('Mount was updated successfully.')->flash();
|
|
||||||
|
|
||||||
return redirect()->route('admin.mounts.view', $mount->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds nodes to the mount's many-to-many relation.
|
|
||||||
*/
|
|
||||||
public function addNodes(Request $request, Mount $mount): RedirectResponse
|
|
||||||
{
|
|
||||||
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
|
|
||||||
|
|
||||||
$nodes = $data['nodes'] ?? [];
|
|
||||||
if (count($nodes) > 0) {
|
|
||||||
$mount->nodes()->attach($nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->alert->success('Mount was updated successfully.')->flash();
|
|
||||||
|
|
||||||
return redirect()->route('admin.mounts.view', $mount->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an egg from the mount's many-to-many relation.
|
|
||||||
*/
|
|
||||||
public function deleteEgg(Mount $mount, int $egg_id): Response
|
|
||||||
{
|
|
||||||
$mount->eggs()->detach($egg_id);
|
|
||||||
|
|
||||||
return response('', 204);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a node from the mount's many-to-many relation.
|
|
||||||
*/
|
|
||||||
public function deleteNode(Mount $mount, int $node_id): Response
|
|
||||||
{
|
|
||||||
$mount->nodes()->detach($node_id);
|
|
||||||
|
|
||||||
return response('', 204);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -56,7 +56,7 @@ class NodeAutoDeployController extends Controller
|
|||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'node' => $node->id,
|
'node' => $node->id,
|
||||||
'token' => $key->identifier . decrypt($key->token),
|
'token' => $key->identifier . $key->token,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,29 +39,8 @@ class NodeViewController extends Controller
|
|||||||
->where('node_id', '=', $node->id)
|
->where('node_id', '=', $node->id)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$usageStats = Collection::make(['disk' => $stats->sum_disk, 'memory' => $stats->sum_memory])
|
|
||||||
->mapWithKeys(function ($value, $key) use ($node) {
|
|
||||||
$maxUsage = $node->{$key};
|
|
||||||
if ($node->{$key . '_overallocate'} > 0) {
|
|
||||||
$maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
$percent = ($value / $maxUsage) * 100;
|
|
||||||
|
|
||||||
return [
|
|
||||||
$key => [
|
|
||||||
'value' => number_format($value),
|
|
||||||
'max' => number_format($maxUsage),
|
|
||||||
'percent' => $percent,
|
|
||||||
'css' => ($percent <= self::THRESHOLD_PERCENTAGE_LOW) ? 'green' : (($percent > self::THRESHOLD_PERCENTAGE_MEDIUM) ? 'red' : 'yellow'),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
return view('admin.nodes.view.index', [
|
return view('admin.nodes.view.index', [
|
||||||
'node' => $node,
|
'node' => $node,
|
||||||
'stats' => $usageStats,
|
|
||||||
'version' => $this->versionService,
|
'version' => $this->versionService,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -117,7 +96,7 @@ class NodeViewController extends Controller
|
|||||||
{
|
{
|
||||||
$this->plainInject([
|
$this->plainInject([
|
||||||
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
|
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
|
||||||
->only(['scheme', 'fqdn', 'daemonListen', 'daemon_token_id', 'daemon_token']),
|
->only(['scheme', 'fqdn', 'daemon_listen', 'daemon_token_id', 'daemon_token']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return view('admin.nodes.view.servers', [
|
return view('admin.nodes.view.servers', [
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ class CreateServerController extends Controller
|
|||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
* @throws \App\Exceptions\DisplayException
|
* @throws \App\Exceptions\DisplayException
|
||||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
|
||||||
* @throws \Throwable
|
* @throws \Throwable
|
||||||
*/
|
*/
|
||||||
public function store(ServerFormRequest $request): RedirectResponse
|
public function store(ServerFormRequest $request): RedirectResponse
|
||||||
|
|||||||
@@ -2,21 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Servers;
|
namespace App\Http\Controllers\Admin\Servers;
|
||||||
|
|
||||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
|
||||||
use App\Models\Allocation;
|
|
||||||
use App\Models\Node;
|
|
||||||
use Carbon\CarbonImmutable;
|
|
||||||
use GuzzleHttp\Exception\TransferException;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Models\Server;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Lcobucci\JWT\Token\Plain;
|
|
||||||
use Prologue\Alerts\AlertsMessageBag;
|
|
||||||
use App\Models\ServerTransfer;
|
|
||||||
use Illuminate\Database\ConnectionInterface;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Services\Nodes\NodeJWTService;
|
use App\Models\Server;
|
||||||
|
use App\Services\Servers\TransferServerService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Prologue\Alerts\AlertsMessageBag;
|
||||||
|
|
||||||
class ServerTransferController extends Controller
|
class ServerTransferController extends Controller
|
||||||
{
|
{
|
||||||
@@ -25,30 +16,10 @@ class ServerTransferController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private AlertsMessageBag $alert,
|
private AlertsMessageBag $alert,
|
||||||
private ConnectionInterface $connection,
|
private TransferServerService $transferServerService,
|
||||||
private NodeJWTService $nodeJWTService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private function notify(Server $server, Plain $token): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
Http::daemon($server->node)->post('/api/transfer', [
|
|
||||||
'json' => [
|
|
||||||
'server_id' => $server->uuid,
|
|
||||||
'url' => $server->node->getConnectionAddress() . "/api/servers/$server->uuid/archive",
|
|
||||||
'token' => 'Bearer ' . $token->toString(),
|
|
||||||
'server' => [
|
|
||||||
'uuid' => $server->uuid,
|
|
||||||
'start_on_completion' => false,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
])->toPsrResponse();
|
|
||||||
} catch (TransferException $exception) {
|
|
||||||
throw new DaemonConnectionException($exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a transfer of a server to a new node.
|
* Starts a transfer of a server to a new node.
|
||||||
*
|
*
|
||||||
@@ -62,85 +33,12 @@ class ServerTransferController extends Controller
|
|||||||
'allocation_additional' => 'nullable',
|
'allocation_additional' => 'nullable',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$node_id = $validatedData['node_id'];
|
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||||
$allocation_id = intval($validatedData['allocation_id']);
|
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
|
||||||
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
|
} else {
|
||||||
|
|
||||||
// Check if the node is viable for the transfer.
|
|
||||||
$node = Node::query()
|
|
||||||
->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemonListen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate'])
|
|
||||||
->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk')
|
|
||||||
->leftJoin('servers', 'servers.node_id', '=', 'nodes.id')
|
|
||||||
->where('nodes.id', $node_id)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$node->isViable($server->memory, $server->disk)) {
|
|
||||||
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
|
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
|
||||||
|
|
||||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$server->validateTransferState();
|
|
||||||
|
|
||||||
$this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) {
|
|
||||||
// Create a new ServerTransfer entry.
|
|
||||||
$transfer = new ServerTransfer();
|
|
||||||
|
|
||||||
$transfer->server_id = $server->id;
|
|
||||||
$transfer->old_node = $server->node_id;
|
|
||||||
$transfer->new_node = $node_id;
|
|
||||||
$transfer->old_allocation = $server->allocation_id;
|
|
||||||
$transfer->new_allocation = $allocation_id;
|
|
||||||
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id')->all();
|
|
||||||
$transfer->new_additional_allocations = $additional_allocations;
|
|
||||||
|
|
||||||
$transfer->save();
|
|
||||||
|
|
||||||
// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
|
|
||||||
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
|
|
||||||
|
|
||||||
// Generate a token for the destination node that the source node can use to authenticate with.
|
|
||||||
$token = $this->nodeJWTService
|
|
||||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
|
||||||
->setSubject($server->uuid)
|
|
||||||
->handle($transfer->newNode, $server->uuid, 'sha256');
|
|
||||||
|
|
||||||
// Notify the source node of the pending outgoing transfer.
|
|
||||||
$this->notify($server, $token);
|
|
||||||
|
|
||||||
return $transfer;
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
|
|
||||||
|
|
||||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
return redirect()->route('admin.servers.view.manage', $server->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns the specified allocations to the specified server.
|
|
||||||
*/
|
|
||||||
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
|
|
||||||
{
|
|
||||||
$allocations = $additional_allocations;
|
|
||||||
$allocations[] = $allocation_id;
|
|
||||||
|
|
||||||
$node = Node::query()->findOrFail($node_id);
|
|
||||||
$unassigned = $node->allocations()
|
|
||||||
->whereNull('server_id')
|
|
||||||
->pluck('id')
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
$updateIds = [];
|
|
||||||
foreach ($allocations as $allocation) {
|
|
||||||
if (!in_array($allocation, $unassigned)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$updateIds[] = $allocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($updateIds)) {
|
|
||||||
Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Servers;
|
namespace App\Http\Controllers\Admin\Servers;
|
||||||
|
|
||||||
|
use App\Enums\ServerState;
|
||||||
use App\Models\DatabaseHost;
|
use App\Models\DatabaseHost;
|
||||||
use App\Models\Egg;
|
use App\Models\Egg;
|
||||||
use App\Models\Mount;
|
use App\Models\Mount;
|
||||||
use App\Models\Node;
|
use App\Models\Node;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Exceptions\DisplayException;
|
use App\Exceptions\DisplayException;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@@ -22,14 +22,14 @@ class ServerViewController extends Controller
|
|||||||
* ServerViewController constructor.
|
* ServerViewController constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private EnvironmentService $environmentService,
|
private readonly EnvironmentService $environmentService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the index view for a server.
|
* Returns the index view for a server.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request, Server $server): View
|
public function index(Server $server): View
|
||||||
{
|
{
|
||||||
return view('admin.servers.view.index', compact('server'));
|
return view('admin.servers.view.index', compact('server'));
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ class ServerViewController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Returns the server details page.
|
* Returns the server details page.
|
||||||
*/
|
*/
|
||||||
public function details(Request $request, Server $server): View
|
public function details(Server $server): View
|
||||||
{
|
{
|
||||||
return view('admin.servers.view.details', compact('server'));
|
return view('admin.servers.view.details', compact('server'));
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ class ServerViewController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Returns a view of server build settings.
|
* Returns a view of server build settings.
|
||||||
*/
|
*/
|
||||||
public function build(Request $request, Server $server): View
|
public function build(Server $server): View
|
||||||
{
|
{
|
||||||
$allocations = $server->node->allocations->toBase();
|
$allocations = $server->node->allocations->toBase();
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ class ServerViewController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Returns the server startup management page.
|
* Returns the server startup management page.
|
||||||
*/
|
*/
|
||||||
public function startup(Request $request, Server $server): View
|
public function startup(Server $server): View
|
||||||
{
|
{
|
||||||
$variables = $this->environmentService->handle($server);
|
$variables = $this->environmentService->handle($server);
|
||||||
$eggs = Egg::all()->keyBy('id');
|
$eggs = Egg::all()->keyBy('id');
|
||||||
@@ -76,7 +76,7 @@ class ServerViewController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Returns all the databases that exist for the server.
|
* Returns all the databases that exist for the server.
|
||||||
*/
|
*/
|
||||||
public function database(Request $request, Server $server): View
|
public function database(Server $server): View
|
||||||
{
|
{
|
||||||
return view('admin.servers.view.database', [
|
return view('admin.servers.view.database', [
|
||||||
'hosts' => DatabaseHost::all(),
|
'hosts' => DatabaseHost::all(),
|
||||||
@@ -87,7 +87,7 @@ class ServerViewController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Returns all the mounts that exist for the server.
|
* Returns all the mounts that exist for the server.
|
||||||
*/
|
*/
|
||||||
public function mounts(Request $request, Server $server): View
|
public function mounts(Server $server): View
|
||||||
{
|
{
|
||||||
$server->load('mounts');
|
$server->load('mounts');
|
||||||
|
|
||||||
@@ -108,9 +108,9 @@ class ServerViewController extends Controller
|
|||||||
*
|
*
|
||||||
* @throws \App\Exceptions\DisplayException
|
* @throws \App\Exceptions\DisplayException
|
||||||
*/
|
*/
|
||||||
public function manage(Request $request, Server $server): View
|
public function manage(Server $server): View
|
||||||
{
|
{
|
||||||
if ($server->status === Server::STATUS_INSTALL_FAILED) {
|
if ($server->status === ServerState::InstallFailed) {
|
||||||
throw new DisplayException('This server is in a failed install state and cannot be recovered. Please delete and re-create the server.');
|
throw new DisplayException('This server is in a failed install state and cannot be recovered. Please delete and re-create the server.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ class ServerViewController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Returns the server deletion page.
|
* Returns the server deletion page.
|
||||||
*/
|
*/
|
||||||
public function delete(Request $request, Server $server): View
|
public function delete(Server $server): View
|
||||||
{
|
{
|
||||||
return view('admin.servers.view.delete', compact('server'));
|
return view('admin.servers.view.delete', compact('server'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Enums\ServerState;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use App\Models\Mount;
|
use App\Models\Mount;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\Database;
|
use App\Models\Database;
|
||||||
use App\Models\MountServer;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Prologue\Alerts\AlertsMessageBag;
|
use Prologue\Alerts\AlertsMessageBag;
|
||||||
use App\Exceptions\DisplayException;
|
use App\Exceptions\DisplayException;
|
||||||
@@ -69,18 +70,22 @@ class ServersController extends Controller
|
|||||||
* @throws \App\Exceptions\DisplayException
|
* @throws \App\Exceptions\DisplayException
|
||||||
* @throws \App\Exceptions\Model\DataValidationException
|
* @throws \App\Exceptions\Model\DataValidationException
|
||||||
*/
|
*/
|
||||||
public function toggleInstall(Server $server): RedirectResponse
|
public function toggleInstall(Server $server)
|
||||||
{
|
{
|
||||||
if ($server->status === Server::STATUS_INSTALL_FAILED) {
|
if ($server->status === ServerState::InstallFailed) {
|
||||||
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
|
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$server->status = $server->isInstalled() ? Server::STATUS_INSTALLING : null;
|
$server->status = $server->isInstalled() ? ServerState::Installing : null;
|
||||||
$server->save();
|
$server->save();
|
||||||
|
|
||||||
$this->alert->success(trans('admin/server.alerts.install_toggled'))->flash();
|
Notification::make()
|
||||||
|
->title('Success!')
|
||||||
|
->body(trans('admin/server.alerts.install_toggled'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
|
||||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,12 +94,15 @@ class ServersController extends Controller
|
|||||||
* @throws \App\Exceptions\DisplayException
|
* @throws \App\Exceptions\DisplayException
|
||||||
* @throws \App\Exceptions\Model\DataValidationException
|
* @throws \App\Exceptions\Model\DataValidationException
|
||||||
*/
|
*/
|
||||||
public function reinstallServer(Server $server): RedirectResponse
|
public function reinstallServer(Server $server)
|
||||||
{
|
{
|
||||||
$this->reinstallService->handle($server);
|
$this->reinstallService->handle($server);
|
||||||
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
|
|
||||||
|
|
||||||
return redirect()->route('admin.servers.view.manage', $server->id);
|
Notification::make()
|
||||||
|
->title('Success!')
|
||||||
|
->body(trans('admin/server.alerts.server_reinstalled'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,7 +133,7 @@ class ServersController extends Controller
|
|||||||
$this->buildModificationService->handle($server, $request->only([
|
$this->buildModificationService->handle($server, $request->only([
|
||||||
'allocation_id', 'add_allocations', 'remove_allocations',
|
'allocation_id', 'add_allocations', 'remove_allocations',
|
||||||
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
|
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
|
||||||
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
|
'database_limit', 'allocation_limit', 'backup_limit', 'oom_killer',
|
||||||
]));
|
]));
|
||||||
} catch (DataValidationException $exception) {
|
} catch (DataValidationException $exception) {
|
||||||
throw new ValidationException($exception->getValidator());
|
throw new ValidationException($exception->getValidator());
|
||||||
@@ -227,12 +235,7 @@ class ServersController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function addMount(Request $request, Server $server): RedirectResponse
|
public function addMount(Request $request, Server $server): RedirectResponse
|
||||||
{
|
{
|
||||||
$mountServer = (new MountServer())->forceFill([
|
$server->mounts()->attach($request->input('mount_id'));
|
||||||
'mount_id' => $request->input('mount_id'),
|
|
||||||
'server_id' => $server->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$mountServer->saveOrFail();
|
|
||||||
|
|
||||||
$this->alert->success('Mount was added successfully.')->flash();
|
$this->alert->success('Mount was added successfully.')->flash();
|
||||||
|
|
||||||
@@ -244,7 +247,7 @@ class ServersController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function deleteMount(Server $server, Mount $mount): RedirectResponse
|
public function deleteMount(Server $server, Mount $mount): RedirectResponse
|
||||||
{
|
{
|
||||||
MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete();
|
$server->mounts()->detach($mount);
|
||||||
|
|
||||||
$this->alert->success('Mount was removed successfully.')->flash();
|
$this->alert->success('Mount was removed successfully.')->flash();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Application\DatabaseHosts;
|
||||||
|
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Models\DatabaseHost;
|
||||||
|
use Spatie\QueryBuilder\QueryBuilder;
|
||||||
|
use App\Services\Databases\Hosts\HostUpdateService;
|
||||||
|
use App\Services\Databases\Hosts\HostCreationService;
|
||||||
|
use App\Transformers\Api\Application\DatabaseHostTransformer;
|
||||||
|
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||||
|
use App\Http\Requests\Api\Application\DatabaseHosts\GetDatabaseHostRequest;
|
||||||
|
use App\Http\Requests\Api\Application\DatabaseHosts\StoreDatabaseHostRequest;
|
||||||
|
use App\Http\Requests\Api\Application\DatabaseHosts\DeleteDatabaseHostRequest;
|
||||||
|
use App\Http\Requests\Api\Application\DatabaseHosts\UpdateDatabaseHostRequest;
|
||||||
|
|
||||||
|
class DatabaseHostController extends ApplicationApiController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* DatabaseHostController constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private HostCreationService $creationService,
|
||||||
|
private HostUpdateService $updateService
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the database hosts currently registered on the Panel.
|
||||||
|
*/
|
||||||
|
public function index(GetDatabaseHostRequest $request): array
|
||||||
|
{
|
||||||
|
$databases = QueryBuilder::for(DatabaseHost::query())
|
||||||
|
->allowedFilters(['name', 'host'])
|
||||||
|
->allowedSorts(['id', 'name', 'host'])
|
||||||
|
->paginate($request->query('per_page') ?? 10);
|
||||||
|
|
||||||
|
return $this->fractal->collection($databases)
|
||||||
|
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a single database host.
|
||||||
|
*/
|
||||||
|
public function view(GetDatabaseHostRequest $request, DatabaseHost $databaseHost): array
|
||||||
|
{
|
||||||
|
return $this->fractal->item($databaseHost)
|
||||||
|
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a new database host on the Panel and return an HTTP/201 response code with the
|
||||||
|
* new database host attached.
|
||||||
|
*
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function store(StoreDatabaseHostRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$databaseHost = $this->creationService->handle($request->validated());
|
||||||
|
|
||||||
|
return $this->fractal->item($databaseHost)
|
||||||
|
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
|
||||||
|
->addMeta([
|
||||||
|
'resource' => route('api.application.databases.view', [
|
||||||
|
'database_host' => $databaseHost->id,
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->respond(201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a database host on the Panel and return the updated record to the user.
|
||||||
|
*
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function update(UpdateDatabaseHostRequest $request, DatabaseHost $databaseHost): array
|
||||||
|
{
|
||||||
|
$databaseHost = $this->updateService->handle($databaseHost->id, $request->validated());
|
||||||
|
|
||||||
|
return $this->fractal->item($databaseHost)
|
||||||
|
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a database host from the Panel.
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function delete(DeleteDatabaseHostRequest $request, DatabaseHost $databaseHost): Response
|
||||||
|
{
|
||||||
|
$databaseHost->delete();
|
||||||
|
|
||||||
|
return $this->returnNoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
165
app/Http/Controllers/Api/Application/Mounts/MountController.php
Normal file
165
app/Http/Controllers/Api/Application/Mounts/MountController.php
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Application\Mounts;
|
||||||
|
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Contracts\Translation\Translator;
|
||||||
|
use Spatie\QueryBuilder\QueryBuilder;
|
||||||
|
use App\Models\Mount;
|
||||||
|
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||||
|
use App\Transformers\Api\Application\MountTransformer;
|
||||||
|
use App\Http\Requests\Api\Application\Mounts\GetMountRequest;
|
||||||
|
use App\Http\Requests\Api\Application\Mounts\StoreMountRequest;
|
||||||
|
use App\Http\Requests\Api\Application\Mounts\DeleteMountRequest;
|
||||||
|
use App\Http\Requests\Api\Application\Mounts\UpdateMountRequest;
|
||||||
|
use App\Exceptions\Service\HasActiveServersException;
|
||||||
|
|
||||||
|
class MountController extends ApplicationApiController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* MountController constructor.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected Translator $translator
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the mounts currently available on the Panel.
|
||||||
|
*/
|
||||||
|
public function index(GetMountRequest $request): array
|
||||||
|
{
|
||||||
|
$mounts = QueryBuilder::for(Mount::query())
|
||||||
|
->allowedFilters(['uuid', 'name'])
|
||||||
|
->allowedSorts(['id', 'uuid'])
|
||||||
|
->paginate($request->query('per_page') ?? 50);
|
||||||
|
|
||||||
|
return $this->fractal->collection($mounts)
|
||||||
|
->transformWith($this->getTransformer(MountTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return data for a single instance of a mount.
|
||||||
|
*/
|
||||||
|
public function view(GetMountRequest $request, Mount $mount): array
|
||||||
|
{
|
||||||
|
return $this->fractal->item($mount)
|
||||||
|
->transformWith($this->getTransformer(MountTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new mount on the Panel. Returns the created mount and an HTTP/201
|
||||||
|
* status response on success.
|
||||||
|
*
|
||||||
|
* @throws \App\Exceptions\Model\DataValidationException
|
||||||
|
*/
|
||||||
|
public function store(StoreMountRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$model = (new Mount())->fill($request->validated());
|
||||||
|
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
|
||||||
|
|
||||||
|
$model->saveOrFail();
|
||||||
|
$mount = $model->fresh();
|
||||||
|
|
||||||
|
return $this->fractal->item($mount)
|
||||||
|
->transformWith($this->getTransformer(MountTransformer::class))
|
||||||
|
->addMeta([
|
||||||
|
'resource' => route('api.application.mounts.view', [
|
||||||
|
'mount' => $mount->id,
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->respond(201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing mount on the Panel.
|
||||||
|
*
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
|
public function update(UpdateMountRequest $request, Mount $mount): array
|
||||||
|
{
|
||||||
|
$mount->forceFill($request->validated())->save();
|
||||||
|
|
||||||
|
return $this->fractal->item($mount)
|
||||||
|
->transformWith($this->getTransformer(MountTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a given mount from the Panel as long as there are no servers
|
||||||
|
* currently attached to it.
|
||||||
|
*
|
||||||
|
* @throws \App\Exceptions\Service\HasActiveServersException
|
||||||
|
*/
|
||||||
|
public function delete(DeleteMountRequest $request, Mount $mount): JsonResponse
|
||||||
|
{
|
||||||
|
if ($mount->servers()->count() > 0) {
|
||||||
|
throw new HasActiveServersException($this->translator->get('exceptions.mount.servers_attached'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$mount->delete();
|
||||||
|
|
||||||
|
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds eggs to the mount's many-to-many relation.
|
||||||
|
*/
|
||||||
|
public function addEggs(Request $request, Mount $mount): array
|
||||||
|
{
|
||||||
|
$validatedData = $request->validate([
|
||||||
|
'eggs' => 'required|exists:eggs,id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$eggs = $validatedData['eggs'] ?? [];
|
||||||
|
if (count($eggs) > 0) {
|
||||||
|
$mount->eggs()->attach($eggs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->fractal->item($mount)
|
||||||
|
->transformWith($this->getTransformer(MountTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds nodes to the mount's many-to-many relation.
|
||||||
|
*/
|
||||||
|
public function addNodes(Request $request, Mount $mount): array
|
||||||
|
{
|
||||||
|
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
|
||||||
|
|
||||||
|
$nodes = $data['nodes'] ?? [];
|
||||||
|
if (count($nodes) > 0) {
|
||||||
|
$mount->nodes()->attach($nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->fractal->item($mount)
|
||||||
|
->transformWith($this->getTransformer(MountTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an egg from the mount's many-to-many relation.
|
||||||
|
*/
|
||||||
|
public function deleteEgg(Mount $mount, int $egg_id): JsonResponse
|
||||||
|
{
|
||||||
|
$mount->eggs()->detach($egg_id);
|
||||||
|
|
||||||
|
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a node from the mount's many-to-many relation.
|
||||||
|
*/
|
||||||
|
public function deleteNode(Mount $mount, int $node_id): JsonResponse
|
||||||
|
{
|
||||||
|
$mount->nodes()->detach($node_id);
|
||||||
|
|
||||||
|
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ class NodeController extends ApplicationApiController
|
|||||||
{
|
{
|
||||||
$nodes = QueryBuilder::for(Node::query())
|
$nodes = QueryBuilder::for(Node::query())
|
||||||
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
|
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
|
||||||
->allowedSorts(['id', 'uuid', 'memory', 'disk'])
|
->allowedSorts(['id', 'uuid', 'memory', 'disk', 'cpu'])
|
||||||
->paginate($request->query('per_page') ?? 50);
|
->paginate($request->query('per_page') ?? 50);
|
||||||
|
|
||||||
return $this->fractal->collection($nodes)
|
return $this->fractal->collection($nodes)
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ use App\Http\Requests\Api\Application\Nodes\GetDeployableNodesRequest;
|
|||||||
|
|
||||||
class NodeDeploymentController extends ApplicationApiController
|
class NodeDeploymentController extends ApplicationApiController
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* NodeDeploymentController constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(private FindViableNodesService $viableNodesService)
|
public function __construct(private FindViableNodesService $viableNodesService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
@@ -21,16 +18,17 @@ class NodeDeploymentController extends ApplicationApiController
|
|||||||
* Finds any nodes that are available using the given deployment criteria. This works
|
* Finds any nodes that are available using the given deployment criteria. This works
|
||||||
* similarly to the server creation process, but allows you to pass the deployment object
|
* similarly to the server creation process, but allows you to pass the deployment object
|
||||||
* to this endpoint and get back a list of all Nodes satisfying the requirements.
|
* to this endpoint and get back a list of all Nodes satisfying the requirements.
|
||||||
*
|
|
||||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
|
||||||
*/
|
*/
|
||||||
public function __invoke(GetDeployableNodesRequest $request): array
|
public function __invoke(GetDeployableNodesRequest $request): array
|
||||||
{
|
{
|
||||||
$data = $request->validated();
|
$data = $request->validated();
|
||||||
$nodes = $this->viableNodesService
|
|
||||||
->setMemory($data['memory'])
|
$nodes = $this->viableNodesService->handle(
|
||||||
->setDisk($data['disk'])
|
$data['memory'] ?? 0,
|
||||||
->handle((int) $request->query('per_page'), (int) $request->query('page'));
|
$data['disk'] ?? 0,
|
||||||
|
$data['cpu'] ?? 0,
|
||||||
|
$data['tags'] ?? $data['location_ids'] ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
return $this->fractal->collection($nodes)
|
return $this->fractal->collection($nodes)
|
||||||
->transformWith($this->getTransformer(NodeTransformer::class))
|
->transformWith($this->getTransformer(NodeTransformer::class))
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ServerController extends ApplicationApiController
|
|||||||
public function index(GetServersRequest $request): array
|
public function index(GetServersRequest $request): array
|
||||||
{
|
{
|
||||||
$servers = QueryBuilder::for(Server::query())
|
$servers = QueryBuilder::for(Server::query())
|
||||||
->allowedFilters(['uuid', 'uuidShort', 'name', 'description', 'image', 'external_id'])
|
->allowedFilters(['uuid', 'uuid_short', 'name', 'description', 'image', 'external_id'])
|
||||||
->allowedSorts(['id', 'uuid'])
|
->allowedSorts(['id', 'uuid'])
|
||||||
->paginate($request->query('per_page') ?? 50);
|
->paginate($request->query('per_page') ?? 50);
|
||||||
|
|
||||||
@@ -50,7 +50,6 @@ class ServerController extends ApplicationApiController
|
|||||||
* @throws \App\Exceptions\DisplayException
|
* @throws \App\Exceptions\DisplayException
|
||||||
* @throws \App\Exceptions\Model\DataValidationException
|
* @throws \App\Exceptions\Model\DataValidationException
|
||||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||||
* @throws \App\Exceptions\Service\Deployment\NoViableNodeException
|
|
||||||
*/
|
*/
|
||||||
public function store(StoreServerRequest $request): JsonResponse
|
public function store(StoreServerRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\Application\Servers;
|
namespace App\Http\Controllers\Api\Application\Servers;
|
||||||
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use App\Models\Server;
|
|
||||||
use App\Services\Servers\SuspensionService;
|
|
||||||
use App\Services\Servers\ReinstallServerService;
|
|
||||||
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
|
|
||||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||||
|
use App\Http\Requests\Api\Application\Servers\ServerWriteRequest;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonServerRepository;
|
||||||
|
use App\Services\Servers\ReinstallServerService;
|
||||||
|
use App\Services\Servers\SuspensionService;
|
||||||
|
use App\Services\Servers\TransferServerService;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
class ServerManagementController extends ApplicationApiController
|
class ServerManagementController extends ApplicationApiController
|
||||||
{
|
{
|
||||||
@@ -16,7 +18,9 @@ class ServerManagementController extends ApplicationApiController
|
|||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ReinstallServerService $reinstallServerService,
|
private ReinstallServerService $reinstallServerService,
|
||||||
private SuspensionService $suspensionService
|
private SuspensionService $suspensionService,
|
||||||
|
private TransferServerService $transferServerService,
|
||||||
|
private DaemonServerRepository $daemonServerRepository,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -57,4 +61,44 @@ class ServerManagementController extends ApplicationApiController
|
|||||||
|
|
||||||
return $this->returnNoContent();
|
return $this->returnNoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a transfer of a server to a new node.
|
||||||
|
*/
|
||||||
|
public function startTransfer(ServerWriteRequest $request, Server $server): Response
|
||||||
|
{
|
||||||
|
$validatedData = $request->validate([
|
||||||
|
'node_id' => 'required|exists:nodes,id',
|
||||||
|
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||||
|
'allocation_additional' => 'nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||||
|
// Transfer started
|
||||||
|
$this->returnNoContent();
|
||||||
|
} else {
|
||||||
|
// Node was not viable
|
||||||
|
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a transfer of a server to a new node.
|
||||||
|
*
|
||||||
|
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
|
||||||
|
*/
|
||||||
|
public function cancelTransfer(ServerWriteRequest $request, Server $server): Response
|
||||||
|
{
|
||||||
|
if (!$transfer = $server->transfer) {
|
||||||
|
// Server is not transferring
|
||||||
|
return new Response('', Response::HTTP_NOT_ACCEPTABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transfer->successful = true;
|
||||||
|
$transfer->save();
|
||||||
|
|
||||||
|
$this->daemonServerRepository->setServer($server)->cancelTransfer();
|
||||||
|
|
||||||
|
return $this->returnNoContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\Client\Servers;
|
namespace App\Http\Controllers\Api\Client\Servers;
|
||||||
|
|
||||||
|
use App\Enums\ServerState;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\Backup;
|
use App\Models\Backup;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
@@ -212,7 +213,7 @@ class BackupController extends ClientApiController
|
|||||||
|
|
||||||
// Update the status right away for the server so that we know not to allow certain
|
// Update the status right away for the server so that we know not to allow certain
|
||||||
// actions against it via the Panel API.
|
// actions against it via the Panel API.
|
||||||
$server->update(['status' => Server::STATUS_RESTORING_BACKUP]);
|
$server->update(['status' => ServerState::RestoringBackup]);
|
||||||
|
|
||||||
$this->daemonRepository->setServer($server)->restore($backup, $url ?? null, $request->input('truncate'));
|
$this->daemonRepository->setServer($server)->restore($backup, $url ?? null, $request->input('truncate'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class StartupController extends ClientApiController
|
|||||||
$startup = $this->startupCommandService->handle($server);
|
$startup = $this->startupCommandService->handle($server);
|
||||||
|
|
||||||
return $this->fractal->collection(
|
return $this->fractal->collection(
|
||||||
$server->variables()->where('user_viewable', true)->get()
|
$server->variables()->orderBy('sort')->where('user_viewable', true)->get()
|
||||||
)
|
)
|
||||||
->transformWith($this->getTransformer(EggVariableTransformer::class))
|
->transformWith($this->getTransformer(EggVariableTransformer::class))
|
||||||
->addMeta([
|
->addMeta([
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use App\Extensions\Backups\BackupManager;
|
|||||||
use App\Extensions\Filesystem\S3Filesystem;
|
use App\Extensions\Filesystem\S3Filesystem;
|
||||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use App\Exceptions\Http\HttpForbiddenException;
|
||||||
|
|
||||||
class BackupRemoteUploadController extends Controller
|
class BackupRemoteUploadController extends Controller
|
||||||
{
|
{
|
||||||
@@ -32,18 +33,32 @@ class BackupRemoteUploadController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function __invoke(Request $request, string $backup): JsonResponse
|
public function __invoke(Request $request, string $backup): JsonResponse
|
||||||
{
|
{
|
||||||
|
// Get the node associated with the request.
|
||||||
|
/** @var \App\Models\Node $node */
|
||||||
|
$node = $request->attributes->get('node');
|
||||||
|
|
||||||
// Get the size query parameter.
|
// Get the size query parameter.
|
||||||
$size = (int) $request->query('size');
|
$size = (int) $request->query('size');
|
||||||
if (empty($size)) {
|
if (empty($size)) {
|
||||||
throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.');
|
throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var \App\Models\Backup $backup */
|
/** @var \App\Models\Backup $model */
|
||||||
$backup = Backup::query()->where('uuid', $backup)->firstOrFail();
|
$model = Backup::query()
|
||||||
|
->where('uuid', $backup)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
// Check that the backup is "owned" by the node making the request. This avoids other nodes
|
||||||
|
// from messing with backups that they don't own.
|
||||||
|
/** @var \App\Models\Server $server */
|
||||||
|
$server = $model->server;
|
||||||
|
if ($server->node_id !== $node->id) {
|
||||||
|
throw new HttpForbiddenException('You do not have permission to access that backup.');
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent backups that have already been completed from trying to
|
// Prevent backups that have already been completed from trying to
|
||||||
// be uploaded again.
|
// be uploaded again.
|
||||||
if (!is_null($backup->completed_at)) {
|
if (!is_null($model->completed_at)) {
|
||||||
throw new ConflictHttpException('This backup is already in a completed state.');
|
throw new ConflictHttpException('This backup is already in a completed state.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +69,7 @@ class BackupRemoteUploadController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The path where backup will be uploaded to
|
// The path where backup will be uploaded to
|
||||||
$path = sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid);
|
$path = sprintf('%s/%s.tar.gz', $model->server->uuid, $model->uuid);
|
||||||
|
|
||||||
// Get the S3 client
|
// Get the S3 client
|
||||||
$client = $adapter->getClient();
|
$client = $adapter->getClient();
|
||||||
@@ -92,7 +107,7 @@ class BackupRemoteUploadController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the upload_id on the backup in the database.
|
// Set the upload_id on the backup in the database.
|
||||||
$backup->update(['upload_id' => $params['UploadId']]);
|
$model->update(['upload_id' => $params['UploadId']]);
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'parts' => $parts,
|
'parts' => $parts,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Extensions\Backups\BackupManager;
|
|||||||
use App\Extensions\Filesystem\S3Filesystem;
|
use App\Extensions\Filesystem\S3Filesystem;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use App\Http\Requests\Api\Remote\ReportBackupCompleteRequest;
|
use App\Http\Requests\Api\Remote\ReportBackupCompleteRequest;
|
||||||
|
use App\Exceptions\Http\HttpForbiddenException;
|
||||||
|
|
||||||
class BackupStatusController extends Controller
|
class BackupStatusController extends Controller
|
||||||
{
|
{
|
||||||
@@ -30,8 +31,22 @@ class BackupStatusController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(ReportBackupCompleteRequest $request, string $backup): JsonResponse
|
public function index(ReportBackupCompleteRequest $request, string $backup): JsonResponse
|
||||||
{
|
{
|
||||||
|
// Get the node associated with the request.
|
||||||
|
/** @var \App\Models\Node $node */
|
||||||
|
$node = $request->attributes->get('node');
|
||||||
|
|
||||||
/** @var \App\Models\Backup $model */
|
/** @var \App\Models\Backup $model */
|
||||||
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
|
$model = Backup::query()
|
||||||
|
->where('uuid', $backup)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
// Check that the backup is "owned" by the node making the request. This avoids other nodes
|
||||||
|
// from messing with backups that they don't own.
|
||||||
|
/** @var \App\Models\Server $server */
|
||||||
|
$server = $model->server;
|
||||||
|
if ($server->node_id !== $node->id) {
|
||||||
|
throw new HttpForbiddenException('You do not have permission to access that backup.');
|
||||||
|
}
|
||||||
|
|
||||||
if ($model->is_successful) {
|
if ($model->is_successful) {
|
||||||
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
|
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\Remote\Servers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Models\Server;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
|
class ServerContainersController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Updates the server container's status on the Panel
|
||||||
|
*/
|
||||||
|
public function status(Server $server, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$status = fluent($request->json()->all())->get('data.new_state');
|
||||||
|
|
||||||
|
cache()->set("servers.$server->uuid.container.status", $status, now()->addHour());
|
||||||
|
|
||||||
|
return new JsonResponse([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\Remote\Servers;
|
namespace App\Http\Controllers\Api\Remote\Servers;
|
||||||
|
|
||||||
|
use App\Enums\ServerState;
|
||||||
use App\Models\Backup;
|
use App\Models\Backup;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
@@ -81,7 +82,7 @@ class ServerDetailsController extends Controller
|
|||||||
->latest('timestamp'),
|
->latest('timestamp'),
|
||||||
])
|
])
|
||||||
->where('node_id', $node->id)
|
->where('node_id', $node->id)
|
||||||
->where('status', Server::STATUS_RESTORING_BACKUP)
|
->where('status', ServerState::RestoringBackup)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$this->connection->transaction(function () use ($node, $servers) {
|
$this->connection->transaction(function () use ($node, $servers) {
|
||||||
@@ -108,7 +109,7 @@ class ServerDetailsController extends Controller
|
|||||||
// Update any server marked as installing or restoring as being in a normal state
|
// Update any server marked as installing or restoring as being in a normal state
|
||||||
// at this point in the process.
|
// at this point in the process.
|
||||||
Server::query()->where('node_id', $node->id)
|
Server::query()->where('node_id', $node->id)
|
||||||
->whereIn('status', [Server::STATUS_INSTALLING, Server::STATUS_RESTORING_BACKUP])
|
->whereIn('status', [ServerState::Installing, ServerState::RestoringBackup])
|
||||||
->update(['status' => null]);
|
->update(['status' => null]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\Remote\Servers;
|
namespace App\Http\Controllers\Api\Remote\Servers;
|
||||||
|
|
||||||
|
use App\Enums\ServerState;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -36,16 +37,16 @@ class ServerInstallController extends Controller
|
|||||||
|
|
||||||
// Make sure the type of failure is accurate
|
// Make sure the type of failure is accurate
|
||||||
if (!$request->boolean('successful')) {
|
if (!$request->boolean('successful')) {
|
||||||
$status = Server::STATUS_INSTALL_FAILED;
|
$status = ServerState::InstallFailed;
|
||||||
|
|
||||||
if ($request->boolean('reinstall')) {
|
if ($request->boolean('reinstall')) {
|
||||||
$status = Server::STATUS_REINSTALL_FAILED;
|
$status = ServerState::ReinstallFailed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the server suspended if it's already suspended
|
// Keep the server suspended if it's already suspended
|
||||||
if ($server->status === Server::STATUS_SUSPENDED) {
|
if ($server->status === ServerState::Suspended) {
|
||||||
$status = Server::STATUS_SUSPENDED;
|
$status = ServerState::Suspended;
|
||||||
}
|
}
|
||||||
|
|
||||||
$previouslyInstalledAt = $server->installed_at;
|
$previouslyInstalledAt = $server->installed_at;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class SftpAuthenticationController extends Controller
|
|||||||
protected function getServer(Request $request, string $uuid): Server
|
protected function getServer(Request $request, string $uuid): Server
|
||||||
{
|
{
|
||||||
return Server::query()
|
return Server::query()
|
||||||
->where(fn ($builder) => $builder->where('uuid', $uuid)->orWhere('uuidShort', $uuid))
|
->where(fn ($builder) => $builder->where('uuid', $uuid)->orWhere('uuid_short', $uuid))
|
||||||
->where('node_id', $request->attributes->get('node')->id)
|
->where('node_id', $request->attributes->get('node')->id)
|
||||||
->firstOr(function () use ($request) {
|
->firstOr(function () use ($request) {
|
||||||
$this->reject($request);
|
$this->reject($request);
|
||||||
|
|||||||
@@ -65,9 +65,7 @@ class LoginCheckpointController extends AbstractLoginController
|
|||||||
return $this->sendLoginResponse($user, $request);
|
return $this->sendLoginResponse($user, $request);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$decrypted = decrypt($user->totp_secret);
|
if ($this->google2FA->verifyKey($user->totp_secret, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
|
||||||
|
|
||||||
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code'), config('panel.auth.2fa.window'))) {
|
|
||||||
Event::dispatch(new ProvidedAuthenticationToken($user));
|
Event::dispatch(new ProvidedAuthenticationToken($user));
|
||||||
|
|
||||||
return $this->sendLoginResponse($user, $request);
|
return $this->sendLoginResponse($user, $request);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class DaemonAuthenticate
|
|||||||
/** @var Node $node */
|
/** @var Node $node */
|
||||||
$node = Node::query()->where('daemon_token_id', $parts[0])->firstOrFail();
|
$node = Node::query()->where('daemon_token_id', $parts[0])->firstOrFail();
|
||||||
|
|
||||||
if (hash_equals((string) decrypt($node->daemon_token), $parts[1])) {
|
if (hash_equals((string) $node->daemon_token, $parts[1])) {
|
||||||
$request->attributes->set('node', $node);
|
$request->attributes->set('node', $node);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests\Admin;
|
|
||||||
|
|
||||||
use App\Models\Mount;
|
|
||||||
|
|
||||||
class MountFormRequest extends AdminFormRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Set up the validation rules to use for these requests.
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
if ($this->method() === 'PATCH') {
|
|
||||||
/** @var Mount $mount */
|
|
||||||
$mount = $this->route()->parameter('mount');
|
|
||||||
|
|
||||||
return Mount::getRulesForUpdate($mount->id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Mount::getRules();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user