Compare commits

...

363 Commits
1.6.4 ... 3.3

Author SHA1 Message Date
Dennis
514c010804 fix 2023-11-20 10:12:21 +01:00
Dennis
b3a5624ad4 fx 2023-11-20 10:08:48 +01:00
Dennis
3857aa33d2 wip 2023-11-20 10:07:09 +01:00
Dennis
c625a5c967 rename this method 2023-11-20 10:06:19 +01:00
Dennis
b4d35adfb4 wip 2023-10-18 08:19:48 +02:00
Dennis
0c1d970a9c cleaning 2023-10-17 16:22:45 +02:00
Dennis
3df600bcda wip 2023-10-07 09:50:11 +02:00
Dennis Smink
c9e0bb3bda Merge pull request #24 from ploi/rjs/provider-plans-per-package
RJS/Assign provider plans to specific packages
2023-10-02 11:14:26 +02:00
Ralph J. Smit
3aae5068ce Simplify 2023-09-28 20:58:54 +02:00
Ralph J. Smit
255353763f Add clarifying comment 2023-09-28 20:57:58 +02:00
Ralph J. Smit
99a49848ca Translations 2023-09-28 20:55:20 +02:00
Ralph J. Smit
def9e3c722 Style 2023-09-28 20:50:07 +02:00
Ralph J. Smit
6cc46cf652 Allow limiting provider plans per package 2023-09-28 20:48:12 +02:00
Ralph J. Smit
9ac72ffda8 Fix headerActions package resource 2023-09-28 13:36:32 +02:00
Dennis
3f1bdb1d8e wip 2023-09-28 13:29:45 +02:00
Dennis
f3d2b0c71f wip 2023-09-28 13:28:51 +02:00
Dennis
4071ba6d49 wip 2023-09-28 13:25:32 +02:00
Dennis Smink
4ec50d7ca1 Merge pull request #23 from ploi/rjs/filament-v3
RJS/Filament V3 upgrade
2023-09-28 13:23:37 +02:00
Ralph J. Smit
51b7f28634 Style 2023-09-28 12:15:55 +02:00
Ralph J. Smit
7f6b59cd4f WIP 2023-09-28 12:15:33 +02:00
Ralph J. Smit
80b4428b72 Filament V3 2023-09-28 12:12:04 +02:00
Dennis
010ecd63ac Merge branch 'develop' of https://github.com/ploi/ploi-core into develop 2023-09-28 08:21:51 +02:00
Dennis
741104de05 wip 2023-09-28 08:21:44 +02:00
Dennis Smink
5254ca3ebe Merge pull request #22 from ploi/rjs/fix-laravel-data
RJS/Upgrade to Laravel 10 – Fix testsuite / Laravel Data implementation
2023-09-27 08:44:45 +02:00
Ralph J. Smit
aefbb5be33 Put attributes on single line 2023-09-26 23:48:48 +02:00
Ralph J. Smit
d22bb52f35 Apply style 2023-09-26 23:47:24 +02:00
Ralph J. Smit
088d951bea Fix Laravel Data/testsuite 2023-09-26 23:44:58 +02:00
Dennis
01fe642a9d wip 2023-08-31 08:18:14 +02:00
Dennis
258e7127f7 wip upgrade 2023-04-03 10:45:59 +02:00
Dennis
7e44db0e56 wip 2023-02-02 11:26:10 +01:00
Dennis Smink
e1c07d84df Merge pull request #21 from ploi-deploy/update-label-tickets
Update SupportTicketResource.php
2023-01-30 11:17:00 +01:00
Dennis Smink
145a4af407 Update SupportTicketResource.php 2023-01-30 11:15:37 +01:00
Dennis
d1a7b6002a wip 2023-01-28 20:06:06 +01:00
Dennis
a9c0bdee34 remove 2023-01-28 20:05:20 +01:00
Dennis
604b535895 wip 2023-01-28 20:04:55 +01:00
Dennis
c67546b949 Merge branch 'develop'
# Conflicts:
#	public/build/manifest.json
2023-01-28 20:00:31 +01:00
Dennis Smink
0a45a1c8b2 Merge pull request #19 from ploi-deploy/nzd-currency
NZD currency
2023-01-28 20:00:02 +01:00
Dennis
3cd83ad69c nzd 2023-01-28 19:57:43 +01:00
GitHub Actions
d27df25b3c Updated build assets 2023-01-25 12:26:54 +00:00
Dennis
e55e984f98 wip 2023-01-25 13:25:23 +01:00
Dennis
5fbc0e0a37 Merge branch 'develop'
# Conflicts:
#	public/build/manifest.json
2023-01-25 13:22:45 +01:00
Dennis Smink
3313786480 Merge pull request #17 from ploi-deploy/fix-user-creation-admin
Fix user creation admin
2023-01-25 13:22:16 +01:00
Dennis Smink
5ed532535b Merge pull request #16 from ploi-deploy/package-updates
Package updates
2023-01-25 13:21:58 +01:00
Dennis
a8361f7b22 force 2023-01-25 13:21:26 +01:00
Dennis
a190ccf8dc wip 2023-01-25 13:01:32 +01:00
GitHub Actions
3f927f9ec1 Updated build assets 2023-01-24 14:46:10 +00:00
Dennis
33692960ff Merge branch 'develop'
# Conflicts:
#	public/build/manifest.json
2023-01-24 15:44:24 +01:00
Dennis
1f79a4e790 bugfix textarea 2023-01-24 15:44:08 +01:00
GitHub Actions
903e1cccc6 Updated build assets 2023-01-12 10:47:05 +00:00
Dennis
3f7f6206bd Merge branch 'develop'
# Conflicts:
#	public/build/assets/403-e96763bb.js
#	public/build/assets/403.184d0b9c.js
#	public/build/assets/403.9643c546.js
#	public/build/assets/404-d9c28ef0.js
#	public/build/assets/404.178f769b.js
#	public/build/assets/404.6a0f48e1.js
#	public/build/assets/Aliases-a6c1e5ac.js
#	public/build/assets/Aliases.516d8088.js
#	public/build/assets/Aliases.5dffda98.js
#	public/build/assets/Apps-9ee57278.js
#	public/build/assets/Apps.68116a71.js
#	public/build/assets/Apps.cf8f7e35.js
#	public/build/assets/Article-aed7d8cd.js
#	public/build/assets/Article.225d2cc5.js
#	public/build/assets/Article.50ee5004.js
#	public/build/assets/Billing-0334c2dc.js
#	public/build/assets/Billing.e20f325d.js
#	public/build/assets/Billing.ffbd6b67.js
#	public/build/assets/BillingError-e18727e6.js
#	public/build/assets/BillingError.540cc4df.js
#	public/build/assets/BillingError.f97e8473.js
#	public/build/assets/Button-938c2a59.js
#	public/build/assets/Button.3d31b0b0.js
#	public/build/assets/Button.482f5d57.js
#	public/build/assets/Certificates-c17def4c.js
#	public/build/assets/Certificates.b2e8d10a.js
#	public/build/assets/Certificates.b7a467e5.js
#	public/build/assets/Closed-15ff2b19.js
#	public/build/assets/Closed.33434c56.js
#	public/build/assets/Closed.c034ca5a.js
#	public/build/assets/ConfirmTwoFactorAuthentication-1e147d31.js
#	public/build/assets/ConfirmTwoFactorAuthentication.227845fb.js
#	public/build/assets/ConfirmTwoFactorAuthentication.442d0995.js
#	public/build/assets/Container-45f4da93.js
#	public/build/assets/Container.44bb93ee.js
#	public/build/assets/Container.f0b4c619.js
#	public/build/assets/Cronjobs-b059a4eb.js
#	public/build/assets/Cronjobs.669e6bc0.js
#	public/build/assets/Cronjobs.686bc7da.js
#	public/build/assets/Databases-e9eba7f6.js
#	public/build/assets/Databases.602e1197.js
#	public/build/assets/Databases.92234ef8.js
#	public/build/assets/Dns-a10c5265.js
#	public/build/assets/Dns.2ab43d8b.js
#	public/build/assets/Dns.4bdaed9c.js
#	public/build/assets/DropdownListItemButton-7877064b.js
#	public/build/assets/DropdownListItemButton.573e0a20.js
#	public/build/assets/DropdownListItemButton.ef103e7c.js
#	public/build/assets/Email-79a8d6e5.js
#	public/build/assets/Email.2626d110.js
#	public/build/assets/Email.e0bd114e.js
#	public/build/assets/EmptyImage-db7f150d.js
#	public/build/assets/EmptyImage.090e8b16.js
#	public/build/assets/EmptyImage.e1281e10.js
#	public/build/assets/Form-125b83ab.js
#	public/build/assets/Form.55885a08.js
#	public/build/assets/Form.e3b24233.js
#	public/build/assets/FormInput-f09111c3.js
#	public/build/assets/FormInput.541a08d4.js
#	public/build/assets/FormInput.7a518a32.js
#	public/build/assets/FormSelect-f8b36700.js
#	public/build/assets/FormSelect.02de001d.js
#	public/build/assets/FormSelect.19ff5254.js
#	public/build/assets/FormTextarea-a69c36b6.js
#	public/build/assets/FormTextarea.d5ee4b96.js
#	public/build/assets/FormTextarea.f69b0fba.js
#	public/build/assets/IconArrowDown-8b1a8522.js
#	public/build/assets/IconArrowDown.262b4357.js
#	public/build/assets/IconArrowDown.ef2fcbd6.js
#	public/build/assets/IconStorage-18f5d16d.js
#	public/build/assets/IconStorage.9205bc74.js
#	public/build/assets/IconStorage.b8642876.js
#	public/build/assets/Index-50b7f2f7.js
#	public/build/assets/Index-65445bf1.js
#	public/build/assets/Index-ae22c003.js
#	public/build/assets/Index-b1914fc4.js
#	public/build/assets/Index-c2ad6517.js
#	public/build/assets/Index-ed2777d6.js
#	public/build/assets/Index.3aca5d17.js
#	public/build/assets/Index.42d835dc.js
#	public/build/assets/Index.4db47acb.js
#	public/build/assets/Index.6d9c51dd.js
#	public/build/assets/Index.76408cb7.js
#	public/build/assets/Index.91265e25.js
#	public/build/assets/Index.b8cdaa18.js
#	public/build/assets/Index.c4ee4e5b.js
#	public/build/assets/Index.c83252d6.js
#	public/build/assets/Index.cdf5b851.js
#	public/build/assets/Index.d203e16d.js
#	public/build/assets/Index.fe71e493.js
#	public/build/assets/InstallationIncomplete-7a0ca111.js
#	public/build/assets/InstallationIncomplete.29d458e0.js
#	public/build/assets/InstallationIncomplete.5ce48727.js
#	public/build/assets/Integrations-02935c41.js
#	public/build/assets/Integrations.c7373676.js
#	public/build/assets/Integrations.e2d78e23.js
#	public/build/assets/Login-0b8af846.js
#	public/build/assets/Login.76b08252.js
#	public/build/assets/Login.9a6f1389.js
#	public/build/assets/MainLayout-8649910c.js
#	public/build/assets/MainLayout.26b583b0.js
#	public/build/assets/MainLayout.da88ac2b.js
#	public/build/assets/ModalContainer-87ab727e.js
#	public/build/assets/ModalContainer.08e5766d.js
#	public/build/assets/ModalContainer.c73e321c.js
#	public/build/assets/Pagination-3f4890e0.js
#	public/build/assets/Pagination.00232add.js
#	public/build/assets/Pagination.a75bec58.js
#	public/build/assets/PasswordCreation-5c8b1f8e.js
#	public/build/assets/PasswordCreation.67495dc1.js
#	public/build/assets/PasswordCreation.84b14c48.js
#	public/build/assets/Privacy-815696be.js
#	public/build/assets/Privacy.99e04234.js
#	public/build/assets/Privacy.fbf4a865.js
#	public/build/assets/Redirects-c6b81f3c.js
#	public/build/assets/Redirects.b99cf7cf.js
#	public/build/assets/Redirects.f8448783.js
#	public/build/assets/Register-0403a2cd.js
#	public/build/assets/Register.a42c47dd.js
#	public/build/assets/Register.d20a1a1b.js
#	public/build/assets/Reset-ddea9d5f.js
#	public/build/assets/Reset.8ac23802.js
#	public/build/assets/Reset.d5c232ca.js
#	public/build/assets/Security-4cd2a6e1.js
#	public/build/assets/Security.0d6b2b10.js
#	public/build/assets/Security.f1214994.js
#	public/build/assets/Settings-e7582a86.js
#	public/build/assets/Settings-efc6bba8.js
#	public/build/assets/Settings-f849221a.js
#	public/build/assets/Settings.168928d6.js
#	public/build/assets/Settings.1c4b0087.js
#	public/build/assets/Settings.52d8d7b4.js
#	public/build/assets/Settings.d667c946.js
#	public/build/assets/Settings.ed66a48f.js
#	public/build/assets/Settings.fadf33ce.js
#	public/build/assets/SettingsLayout-1f4f1c24.js
#	public/build/assets/SettingsLayout.6029cb54.js
#	public/build/assets/SettingsLayout.94ea8a51.js
#	public/build/assets/SettingsSegment-70fda3a9.js
#	public/build/assets/SettingsSegment.8f3bca2b.js
#	public/build/assets/SettingsSegment.e4964203.js
#	public/build/assets/Show-07f07f81.js
#	public/build/assets/Show-2e78bbbc.js
#	public/build/assets/Show-34adf7eb.js
#	public/build/assets/Show-3c892a01.js
#	public/build/assets/Show.0ad83d86.js
#	public/build/assets/Show.4d8ab56a.js
#	public/build/assets/Show.6eaded56.js
#	public/build/assets/Show.859411a2.js
#	public/build/assets/Show.931c3baa.js
#	public/build/assets/Show.ab8d88e8.js
#	public/build/assets/Show.c67e74f4.js
#	public/build/assets/Show.f31307aa.js
#	public/build/assets/TabBar-ad9b2a96.js
#	public/build/assets/TabBar.8304c776.js
#	public/build/assets/TabBar.e7e4c38d.js
#	public/build/assets/TableData-47a3f1fd.js
#	public/build/assets/TableData.4c8a41d7.js
#	public/build/assets/TableData.764d232d.js
#	public/build/assets/Tabs-38ad7844.js
#	public/build/assets/Tabs-66e3833f.js
#	public/build/assets/Tabs-7ea7e6ee.js
#	public/build/assets/Tabs.0753e723.js
#	public/build/assets/Tabs.08e17dfd.js
#	public/build/assets/Tabs.3e1b5c30.js
#	public/build/assets/Tabs.7520b2c6.js
#	public/build/assets/Tabs.9c81a864.js
#	public/build/assets/Tabs.bba69a14.js
#	public/build/assets/Terms-dff83575.js
#	public/build/assets/Terms.0c8ae423.js
#	public/build/assets/Terms.aae13061.js
#	public/build/assets/TextDivider-8ebb8335.js
#	public/build/assets/TextDivider.ab7f414c.js
#	public/build/assets/TextDivider.f06d5c66.js
#	public/build/assets/TopBar-149e0829.js
#	public/build/assets/TopBar-43058ee7.js
#	public/build/assets/TopBar-60517658.js
#	public/build/assets/TopBar-97b50929.js
#	public/build/assets/TopBar-fa507151.js
#	public/build/assets/TopBar-fdcb98f3.js
#	public/build/assets/TopBar.23526c20.js
#	public/build/assets/TopBar.3ed45d47.js
#	public/build/assets/TopBar.42fc17aa.js
#	public/build/assets/TopBar.487eeaa9.js
#	public/build/assets/TopBar.544193fc.js
#	public/build/assets/TopBar.6e42637b.js
#	public/build/assets/TopBar.8509fd3d.js
#	public/build/assets/TopBar.a427a47e.js
#	public/build/assets/TopBar.aecff8e2.js
#	public/build/assets/TopBar.bd25c71c.js
#	public/build/assets/TopBar.ddabe973.js
#	public/build/assets/TopBar.e6994b25.js
#	public/build/assets/TwoFactorAuthentication-d10b254e.js
#	public/build/assets/TwoFactorAuthentication.bf9d9f46.js
#	public/build/assets/TwoFactorAuthentication.c3412bf0.js
#	public/build/assets/app-9a1c122c.js
#	public/build/assets/app.67ed15d7.js
#	public/build/assets/app.997ea3ab.js
#	public/build/assets/confirm-eb12c83b.js
#	public/build/assets/confirm.450efa22.js
#	public/build/assets/confirm.91e5714b.js
#	public/build/assets/notification-c544471b.js
#	public/build/assets/notification.544829a1.js
#	public/build/assets/notification.fa833402.js
#	public/build/manifest.json
2023-01-12 11:45:17 +01:00
Dennis
be00824f59 wip 2023-01-12 11:44:17 +01:00
Dennis
eb4220adc9 wip 2022-12-19 11:39:21 +01:00
GitHub Actions
3562a60461 Updated build assets 2022-10-26 06:33:52 +00:00
Dennis
4d80f26519 Merge branch 'develop' 2022-10-26 08:32:42 +02:00
Dennis
29dc893806 remove this 2022-10-26 08:32:37 +02:00
Dennis
35abe4cfd7 Merge branch 'develop' 2022-10-26 07:56:58 +02:00
Dennis
275b359b53 w 2022-10-26 07:56:53 +02:00
Dennis
0be4f4cd94 Merge branch 'develop' 2022-10-26 07:54:10 +02:00
Dennis
2ffd09877e wip 2022-10-26 07:52:43 +02:00
Dennis
a5d2445e3f Fix #10 2022-10-26 07:27:31 +02:00
Dennis
92631cdee9 Merge branch 'develop' 2022-10-17 10:40:29 +02:00
Dennis
0f63b8153a wip 2022-10-17 10:40:20 +02:00
Dennis
b6983d5377 Merge branch 'develop' 2022-10-10 13:31:28 +02:00
Dennis
78899bef61 wip 2022-10-10 13:31:22 +02:00
Dennis
9397651515 Merge branch 'develop' 2022-09-26 08:13:55 +02:00
Dennis
f692fb681a fix 2022-09-26 08:13:49 +02:00
Dennis
6de17d3e3c Merge branch 'develop' 2022-09-20 13:20:01 +02:00
Dennis
b2b24db2e6 wip 2022-09-20 13:19:56 +02:00
Dennis
2af546643e Merge branch 'develop' 2022-09-19 11:07:42 +02:00
Dennis
e63b13e5fd package updates 2022-09-19 11:07:35 +02:00
Dennis
36385c2242 wip 2022-09-11 08:41:00 +02:00
Dennis
ed67a44f5f wip 2022-09-10 16:51:00 +02:00
Dennis
fb8b2fa935 Merge branch 'develop' 2022-09-10 16:50:21 +02:00
Dennis
6e1b7613e4 bugfix 2022-09-10 16:50:08 +02:00
Dennis
175b104ebc wip 2022-08-30 10:09:06 +02:00
Dennis
a72a2466ef Merge branch 'develop' 2022-08-30 09:57:53 +02:00
Dennis
9952e2226d Fix 2022-08-30 09:57:47 +02:00
Dennis
5fa9c334ee prod 2022-08-27 20:04:00 +02:00
Dennis
d1f50c8dd9 prod 2022-08-27 20:03:25 +02:00
Dennis
557cff5e05 Merge branch 'develop' 2022-08-27 20:02:51 +02:00
Dennis
fd1d31d347 fix 2022-08-27 20:02:31 +02:00
Dennis
71d5521ca8 wip 2022-08-19 10:34:24 +02:00
Dennis
489d39099f wip 2022-08-19 10:16:54 +02:00
Dennis
cbd6a227ee Merge branch 'develop'
# Conflicts:
#	public/build/assets/app.1ce28008.css
#	public/js/1535.js
#	public/js/1587.js
#	public/js/1603.js
#	public/js/1970.js
#	public/js/1984.js
#	public/js/2035.js
#	public/js/2502.js
#	public/js/2669.js
#	public/js/2741.js
#	public/js/3067.js
#	public/js/344.js
#	public/js/3495.js
#	public/js/3506.js
#	public/js/3686.js
#	public/js/4104.js
#	public/js/4282.js
#	public/js/4482.js
#	public/js/4598.js
#	public/js/4627.js
#	public/js/4710.js
#	public/js/4967.js
#	public/js/5035.js
#	public/js/5168.js
#	public/js/547.js
#	public/js/5503.js
#	public/js/5597.js
#	public/js/5727.js
#	public/js/5867.js
#	public/js/630.js
#	public/js/6341.js
#	public/js/6366.js
#	public/js/6658.js
#	public/js/6734.js
#	public/js/6887.js
#	public/js/696.js
#	public/js/6974.js
#	public/js/7007.js
#	public/js/7022.js
#	public/js/7319.js
#	public/js/7414.js
#	public/js/7644.js
#	public/js/7790.js
#	public/js/7942.js
#	public/js/8162.js
#	public/js/8399.js
#	public/js/8502.js
#	public/js/8665.js
#	public/js/8899.js
#	public/js/8968.js
#	public/js/9039.js
#	public/js/9049.js
#	public/js/9053.js
#	public/js/917.js
#	public/js/9202.js
#	public/js/9488.js
#	public/js/9617.js
#	public/js/9737.js
#	public/js/9972.js
#	public/js/9989.js
#	public/js/9991.js
#	public/js/app.js
2022-08-19 09:39:27 +02:00
Dennis
b3d2b2ec7a demo fix 2022-08-19 09:38:37 +02:00
Dennis
2889e46685 wip 2022-08-19 09:31:12 +02:00
Dennis
761878e1b6 w 2022-08-19 09:30:44 +02:00
Dennis
4169b07b39 wip 2022-08-19 09:28:50 +02:00
Dennis
8c001cf984 wip 2022-08-19 09:25:01 +02:00
Dennis
a73adb9acf wip 2022-08-18 13:34:40 +02:00
Dennis
6ed9867f6b make this 8.1 default 2022-08-17 10:45:29 +02:00
Dennis
b1667ff445 wip 2022-08-17 09:59:02 +02:00
Dennis
db4a25fb5c fix 2022-08-17 09:55:29 +02:00
Dennis
ee21924253 fix tests 2022-08-17 09:53:08 +02:00
Dennis
37ea8aa6b0 remove old files 2022-08-17 09:33:12 +02:00
Dennis
d4c495eaee wip 2022-08-17 09:32:33 +02:00
Dennis
434046cd3e PSR formatting 2022-08-17 08:20:40 +02:00
Ralph J. Smit
eee61494d0 Fix policies interfering with admin 2022-08-16 14:27:35 +02:00
Ralph J. Smit
053864a589 Clarify that servers are also deleted 2022-08-16 13:20:55 +02:00
Ralph J. Smit
dc87d0d415 Allow completely deleting a user with all the sites etc. 2022-08-16 13:20:23 +02:00
Ralph J. Smit
ae1b41e068 Add language and requires_pasword_for_ftp to User API endpoints 2022-08-16 13:19:53 +02:00
Ralph J. Smit
277aae4bca Fix polling resets didn't preserve the form state 2022-08-16 13:19:20 +02:00
Dennis
f727c4cddb wip 2022-08-16 08:22:29 +02:00
Dennis
58c91fe7bc wip 2022-08-16 08:15:05 +02:00
Dennis
cc29f729d3 Force 8.1 2022-08-15 13:48:37 +02:00
Dennis
ff8982c2ee w 2022-08-15 10:06:14 +02:00
Dennis
aab1e86eee Wip 2022-08-15 09:29:47 +02:00
Ralph J. Smit
3388b4fdec Add packages API endpoint 2022-08-12 22:20:56 +02:00
Ralph J. Smit
8ef6e2b64c Update ServerResource.php 2022-08-12 21:14:59 +02:00
Ralph J. Smit
dd5c074976 Rework title components 2022-08-12 21:11:23 +02:00
Ralph J. Smit
9861ff3a9b Rescue Horizon exceptions 2022-08-09 11:06:39 +02:00
Ralph J. Smit
b4607d8e01 Update SiteResource.php 2022-08-09 11:00:44 +02:00
Dennis
7a98295853 wip 2022-08-09 09:23:31 +02:00
Dennis
95e7682cc2 wip 2022-08-09 09:18:37 +02:00
Dennis
251c29b4bd wip 2022-08-09 09:13:42 +02:00
Ralph J. Smit
96587db6a2 Update synchronize actions in main index pages, hide for busy servers/sites to prevent exceptions 2022-08-08 17:31:18 +02:00
Ralph J. Smit
06aecba93d Add a site relation managers 2022-08-08 17:26:42 +02:00
Ralph J. Smit
bcadd716fe Extract synchronization logic into separate pages 2022-08-08 17:21:58 +02:00
Ralph J. Smit
b2e1ee9e24 Finish reset db password 2022-08-08 12:07:01 +02:00
Ralph J. Smit
e3612ebf23 Merge branch 'V3' of https://github.com/ploi-deploy/ploi-core into V3 2022-08-08 11:57:48 +02:00
Ralph J. Smit
25d5a9617d WIP password reset 2022-08-08 11:57:37 +02:00
Dennis
b6e25806ea Merge branch 'V3' of https://github.com/ploi-deploy/ploi-core into V3 2022-08-08 11:53:59 +02:00
Dennis
f0a8e5e318 wip 2022-08-08 11:53:55 +02:00
Ralph J. Smit
c5dbfa2c4b Reset TwoFactorAuthentication 2022-08-08 11:09:13 +02:00
Ralph J. Smit
93273b5a45 Refactor to $touches 2022-08-08 10:20:08 +02:00
Ralph J. Smit
751449de5e Add Support ticket badge 2022-08-08 10:19:49 +02:00
Ralph J. Smit
e601222b4f Tweak system page design 2022-08-06 17:32:12 +02:00
Ralph J. Smit
30685c4595 Remove files related to V2 admin panel 2022-08-06 17:24:24 +02:00
Ralph J. Smit
60c951a1f8 Build files 2022-08-06 17:20:41 +02:00
Ralph J. Smit
ccc09f0967 Replace inertia link to dashboard with regular link 2022-08-06 17:19:08 +02:00
Ralph J. Smit
de9834d6a7 Upgrade to Vue 3 & use correct Inertia createApp 2022-08-06 17:11:57 +02:00
Ralph J. Smit
09dd3db506 Update GitHub actions script for Filament stylesheet 2022-08-06 15:46:06 +02:00
Ralph J. Smit
560100e592 Build assets 2022-08-06 15:45:28 +02:00
Ralph J. Smit
0f5ef71936 Create manifest.json 2022-08-06 15:43:19 +02:00
Ralph J. Smit
84b2f36440 Fix markdown support ticket 2022-08-06 15:37:37 +02:00
Ralph J. Smit
92c605d9df Cleanup 2022-08-06 15:36:36 +02:00
Ralph J. Smit
0fddf4f348 Support ticket system 2022-08-06 15:36:27 +02:00
Ralph J. Smit
ef64bdd7b3 Update DocumentationItemResource.php 2022-08-06 15:35:57 +02:00
Ralph J. Smit
465b2a524b Update setting() function to support storing multiple settings at the same time 2022-08-06 15:35:45 +02:00
Ralph J. Smit
f2c47ba2f3 Update icons and order 2022-08-05 18:29:18 +02:00
Ralph J. Smit
eab8c45f57 Update DocumentationCategory.php 2022-08-05 18:28:21 +02:00
Ralph J. Smit
2215e12717 Fix empty logo 2022-08-05 18:28:18 +02:00
Ralph J. Smit
f76e5b4d7b Add documentation functionality 2022-08-05 18:28:10 +02:00
Ralph J. Smit
9d1b12b0a3 Admin panel v1 2022-08-05 18:11:23 +02:00
Ralph J. Smit
d89482c4aa WIP 2022-08-01 20:54:52 +02:00
Ralph J. Smit
488808a7a2 WIP 2022-07-30 20:48:39 +02:00
Dennis
8e788dfd8e wip 2022-07-29 08:53:10 +02:00
Dennis
200d2bc44d wip 2022-07-29 08:47:02 +02:00
Dennis
f9424781c0 w 2022-07-29 08:42:34 +02:00
Dennis
f2441990cd w 2022-07-27 13:39:11 +02:00
Dennis
369d205d6a wip 2022-07-27 13:36:18 +02:00
Dennis
b63d4e753b Merge branch '120-move-mix-to-vite' into V3
# Conflicts:
#	composer.lock
#	package.json
#	public/css/app.css
#	public/js/app.js
#	resources/deprecated/Admin/Alerts/Create.vue
#	resources/deprecated/Admin/Alerts/Edit.vue
#	resources/deprecated/Admin/Alerts/Index.vue
#	resources/deprecated/Admin/Documentation/Articles/Create.vue
#	resources/deprecated/Admin/Documentation/Articles/Edit.vue
#	resources/deprecated/Admin/Documentation/Articles/Index.vue
#	resources/deprecated/Admin/Documentation/Create.vue
#	resources/deprecated/Admin/Documentation/Edit.vue
#	resources/deprecated/Admin/Documentation/Index.vue
#	resources/deprecated/Admin/Packages/Create.vue
#	resources/deprecated/Admin/Packages/Edit.vue
#	resources/deprecated/Admin/Packages/Index.vue
#	resources/deprecated/Admin/Servers/Index.vue
#	resources/deprecated/Admin/Services/Index.vue
#	resources/deprecated/Admin/Services/Provider/Edit.vue
#	resources/deprecated/Admin/Services/Providers.vue
#	resources/deprecated/Admin/Services/Server/Edit.vue
#	resources/deprecated/Admin/Services/Servers.vue
#	resources/deprecated/Admin/Services/Site/Edit.vue
#	resources/deprecated/Admin/Services/Sites.vue
#	resources/deprecated/Admin/Sites/Index.vue
#	resources/deprecated/Admin/Support/Index.vue
#	resources/deprecated/Admin/Support/Show.vue
#	resources/deprecated/Admin/Terms.vue
#	resources/deprecated/Admin/Users/Create.vue
#	resources/deprecated/Admin/Users/Edit.vue
#	resources/deprecated/Admin/Users/Index.vue
#	resources/deprecated/Admin/Users/Show.vue
#	resources/js/Pages/Sites/Dns.vue
2022-07-27 13:30:56 +02:00
Dennis
fbe7641cb7 wip 2022-07-27 13:30:13 +02:00
Ralph J. Smit
1ebcf75806 Improve DNS editor 2022-07-27 12:31:15 +02:00
Ralph J. Smit
88b324b47a WIP 2022-07-26 21:32:56 +02:00
Dennis
50768d5648 Merge branch 'develop' 2022-07-24 12:51:08 +02:00
Ralph J. Smit
7cfe8d64c4 Updates to endpoints 2022-07-23 16:42:23 +02:00
Ralph J. Smit
f6f5385751 Update .gitignore 2022-07-23 16:41:58 +02:00
Dennis Smink
62084f590e wip 2022-07-21 15:24:59 +02:00
Dennis Smink
c7edf262f6 Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/1042.js
#	public/js/1304.js
#	public/js/1438.js
#	public/js/1582.js
#	public/js/1973.js
#	public/js/2232.js
#	public/js/2306.js
#	public/js/2426.js
#	public/js/2658.js
#	public/js/2668.js
#	public/js/2693.js
#	public/js/2940.js
#	public/js/2999.js
#	public/js/3038.js
#	public/js/3292.js
#	public/js/3739.js
#	public/js/4144.js
#	public/js/43.js
#	public/js/4379.js
#	public/js/4485.js
#	public/js/4596.js
#	public/js/4695.js
#	public/js/47.js
#	public/js/4710.js
#	public/js/4766.js
#	public/js/4791.js
#	public/js/4804.js
#	public/js/4946.js
#	public/js/5641.js
#	public/js/565.js
#	public/js/5766.js
#	public/js/5865.js
#	public/js/6038.js
#	public/js/6282.js
#	public/js/6289.js
#	public/js/6340.js
#	public/js/6424.js
#	public/js/6511.js
#	public/js/6545.js
#	public/js/684.js
#	public/js/6861.js
#	public/js/701.js
#	public/js/7054.js
#	public/js/7210.js
#	public/js/7399.js
#	public/js/7611.js
#	public/js/7767.js
#	public/js/8027.js
#	public/js/8309.js
#	public/js/8371.js
#	public/js/8607.js
#	public/js/8786.js
#	public/js/883.js
#	public/js/908.js
#	public/js/9195.js
#	public/js/9281.js
#	public/js/9353.js
#	public/js/app.js
2022-07-21 15:24:23 +02:00
Ralph J. Smit
4294377160 WIP 2022-07-20 16:53:18 +02:00
Ralph J. Smit
1ae9519594 WIP 2022-07-20 16:47:01 +02:00
Ralph J. Smit
c0f7d572cb Update GitHub Actions trigger 2022-07-20 16:06:01 +02:00
Ralph J. Smit
247fd27368 Merge branch 'develop-test' of https://github.com/ploi-deploy/ploi-core into develop-test 2022-07-20 16:05:13 +02:00
GitHub Actions
39e5d20444 Run Laravel Mix en build front-end assets 2022-07-20 14:04:52 +00:00
Ralph J. Smit
8900e5088a Update master.yml 2022-07-20 16:03:59 +02:00
Ralph J. Smit
e52cc6205a Revert "Dummy change to trigger GH Action"
This reverts commit bffaadec1b.
2022-07-20 16:03:15 +02:00
GitHub Actions
a85be2d666 Run Laravel Mix en build front-end assets 2022-07-20 14:02:51 +00:00
Ralph J. Smit
bffaadec1b Dummy change to trigger GH Action 2022-07-20 16:01:22 +02:00
GitHub Actions
0c7398c7ac Run Laravel Mix en build front-end assets 2022-07-20 13:59:07 +00:00
Ralph J. Smit
a44ce140fc Fix test 2022-07-20 15:57:25 +02:00
Ralph J. Smit
8afe483cee Update PHP 8.0 compatible lock file 2022-07-20 15:55:09 +02:00
Ralph J. Smit
9bd3107303 Node 14 for Vite 2022-07-20 15:53:30 +02:00
Ralph J. Smit
5b124cc433 Update master.yml 2022-07-20 15:52:36 +02:00
Ralph J. Smit
661426668e Update .gitignore 2022-07-20 15:52:34 +02:00
Ralph J. Smit
2df37131d1 Build assets #1 2022-07-20 15:49:48 +02:00
Ralph J. Smit
c55b2f1f85 Merge 2022-07-20 15:49:25 +02:00
Ralph J. Smit
705739d2d3 Merge branch '120-move-mix-to-vite' into develop-test 2022-07-20 15:47:01 +02:00
Ralph J. Smit
35bf27097a Update app.blade.php 2022-07-20 15:44:46 +02:00
Ralph J. Smit
59d60a5b03 Import CSS in JS 2022-07-20 15:40:40 +02:00
Ralph J. Smit
0cd7c7e4f6 Remove Mix files 2022-07-20 15:37:55 +02:00
Ralph J. Smit
254dbaf2ec Fix page title 2022-07-20 15:36:49 +02:00
Ralph J. Smit
705104524e Fix typo 2022-07-20 15:36:46 +02:00
Ralph J. Smit
a7da48a5dd Fix undefined TabBars error 2022-07-20 15:36:35 +02:00
Ralph J. Smit
df87d541cd Fix Inertia, except TabBars 2022-07-20 15:27:47 +02:00
Ralph J. Smit
07ba298c5e WIP Vite 2022-07-20 14:50:11 +02:00
Ralph J. Smit
1ce6e8cace Fix test 2022-07-20 10:51:43 +02:00
Dennis Smink
57f783490b ww 2022-07-20 08:26:14 +02:00
Dennis Smink
eaab262629 Merge branch '61-ploi-core-design' into develop
# Conflicts:
#	public/js/app.js
2022-07-20 08:26:10 +02:00
Dennis Smink
e2e05f9cbf wip 2022-07-20 08:25:33 +02:00
Dennis Smink
24ce8bc60d Merge branch '26-duplicate-check' into develop
# Conflicts:
#	app/Http/Controllers/SiteController.php
#	package-lock.json
#	public/js/app.js
2022-07-20 08:17:59 +02:00
Dennis Smink
75592aaeb2 Do this check server sided 2022-07-20 08:16:42 +02:00
Dennis Smink
c11ad19220 w 2022-07-20 08:08:09 +02:00
Dennis Smink
db799a7d6a w 2022-07-20 08:06:53 +02:00
Dennis Smink
7fea371857 Merge branch '25-add-ssl-for-alias-domains-automatically' into develop
# Conflicts:
#	public/js/app.js
2022-07-20 08:06:48 +02:00
Dennis Smink
d7b3899e71 Few fixes 2022-07-20 08:06:37 +02:00
Dennis Smink
e2886fb67e Merge branch '131-set-up-github-action-for-ploi-core' into develop 2022-07-20 07:56:19 +02:00
Ralph J. Smit
8936e4c2d5 Update package-lock.json 2022-07-19 21:46:38 +02:00
Ralph J. Smit
92bb321a68 WIP 2022-07-19 21:46:33 +02:00
Ralph J. Smit
57c8997dd0 Fix 2022-07-19 21:32:14 +02:00
Ralph J. Smit
2df031a60f Check whether domain already exists when creating site 2022-07-19 21:17:21 +02:00
Ralph J. Smit
77384a1abe Request new certificate automatically after creating new alias 2022-07-19 19:04:01 +02:00
Ralph J. Smit
7b20082537 Implement real usernames for site system users and database name site prefix 2022-07-19 15:58:50 +02:00
Ralph J. Smit
3531e4b296 Update master.yml 2022-07-19 15:21:03 +02:00
Ralph J. Smit
4e92501985 Update master.yml 2022-07-19 15:17:20 +02:00
GitHub Actions
d7632d8289 Run Laravel Mix en build front-end assets 2022-07-19 13:12:48 +00:00
Ralph J. Smit
f404b3e9c6 Update master.yml 2022-07-19 15:11:01 +02:00
Ralph J. Smit
730f9b7451 Update run-tests.yml 2022-07-19 15:09:40 +02:00
Ralph J. Smit
2fe5fd70c9 PHP 8.0 support 2022-07-19 15:07:38 +02:00
Ralph J. Smit
6afe8738df Consistent scripts 2022-07-19 14:50:28 +02:00
Ralph J. Smit
761a940abd Add support for PHP 8.0 back 2022-07-19 14:49:08 +02:00
Ralph J. Smit
f87c1dd5ee Update run-tests.yml 2022-07-19 14:45:06 +02:00
Ralph J. Smit
de70310c90 Update run-tests.yml 2022-07-19 14:43:50 +02:00
Ralph J. Smit
11f9b1ed48 Update run-tests.yml 2022-07-19 14:42:28 +02:00
Ralph J. Smit
060a6b72a7 Update master.yml 2022-07-19 14:42:00 +02:00
Ralph J. Smit
70cc81f110 Update run-tests.yml 2022-07-19 14:41:26 +02:00
Ralph J. Smit
bcc1a9b9a8 Update master.yml 2022-07-19 14:39:10 +02:00
Ralph J. Smit
1c601a6efd Update master.yml 2022-07-19 14:36:42 +02:00
Ralph J. Smit
fee31d03a7 Update master.yml 2022-07-19 14:35:14 +02:00
Ralph J. Smit
b09dc1ba9d Update run-tests.yml 2022-07-19 14:35:11 +02:00
Ralph J. Smit
dae15c620b Update run-tests.yml 2022-07-19 14:31:36 +02:00
Ralph J. Smit
996a048a76 Update phpunit.xml 2022-07-19 14:30:46 +02:00
Ralph J. Smit
8c20f23dfd Update Pest.php 2022-07-19 14:27:27 +02:00
Ralph J. Smit
c80818df4c Update phpunit.xml 2022-07-19 14:19:10 +02:00
Ralph J. Smit
d5e77ae31f Update master.yml 2022-07-19 13:34:03 +02:00
Ralph J. Smit
a14d2c44a1 wip 2022-07-19 13:33:24 +02:00
Ralph J. Smit
3048747ed6 WIP 2022-07-18 22:45:22 +02:00
Ralph J. Smit
20bf6c4784 Compatibility with Http-facade instead of Guzzle 2022-07-18 22:42:24 +02:00
Ralph J. Smit
9b02be5be1 Update SiteController.php 2022-07-18 22:42:04 +02:00
Ralph J. Smit
d141503b6f Remove unused class 2022-07-18 22:26:31 +02:00
Ralph J. Smit
6a8e4e8edf Move Http-tests to Tests/Feature 2022-07-18 22:20:42 +02:00
Ralph J. Smit
89bbf44b3b Finish API-endpoints & tests 2022-07-18 22:19:45 +02:00
Ralph J. Smit
db1f40bf6f WIP 2022-07-16 21:44:33 +02:00
Ralph J. Smit
5933a06dd3 WIP 2022-07-05 18:50:34 +02:00
Ralph J. Smit
3c510906ee General stuff 2022-07-01 21:31:35 +02:00
Ralph J. Smit
b43f4cf292 Server testing & endpoints, general 2022-07-01 21:31:27 +02:00
Ralph J. Smit
9cc046eeed Site testing & endpoints 2022-07-01 21:30:36 +02:00
Ralph J. Smit
8291ac6714 Test and update UserController endpoint 2022-07-01 21:17:53 +02:00
Ralph J. Smit
c578ee70c0 Prepare Pest 2022-07-01 21:17:22 +02:00
Ralph J. Smit
90501e37fd Prepare data objects (hopefully remove some code after PR accepted) 2022-07-01 21:15:59 +02:00
Ralph J. Smit
ec45b0dac0 Update RouteServiceProvider.php 2022-07-01 12:01:33 +02:00
Ralph J. Smit
34b838c259 Implement & test API-authentication, simplify Api-routes 2022-07-01 12:00:16 +02:00
Dennis
28ffc8e240 Merge branch '129-add-user-friendly-404-pages' into develop 2022-07-01 07:53:44 +02:00
Dennis
c9179fbf90 proper mix 2022-07-01 07:52:51 +02:00
Dennis
fe5268971a Merge branch '114-refresh-system-version-in-system-tab' into develop
# Conflicts:
#	public/js/app.js
2022-07-01 07:52:21 +02:00
Dennis
8c246e2dba wip tests 2022-07-01 07:51:27 +02:00
Dennis
81fcfac803 Merge branch '76-the-use-of-html-tags-or-markdown-in-alert-messages' into develop
# Conflicts:
#	public/js/app.js
2022-07-01 07:50:17 +02:00
Ralph J. Smit
ff22b96a8d Fix 404 and 403 Vue error pages 2022-06-30 18:47:11 +02:00
Ralph J. Smit
4a2faf0bce Build files 2022-06-30 18:18:34 +02:00
Ralph J. Smit
5c39d07bf5 Allow version refresh in System 2022-06-30 18:14:54 +02:00
Ralph J. Smit
9870aec79f Support markdown & line breaks in Alert messages 2022-06-30 16:11:18 +02:00
Dennis
833a03e992 dev 2022-06-30 16:02:11 +02:00
Ralph J. Smit
e074ab5be4 Implement two-factor authentication 2022-06-30 15:26:44 +02:00
Dennis
b5963693e6 Prod mix 2022-06-27 10:26:40 +02:00
Dennis
1b7ea67fde Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2022-06-27 10:25:39 +02:00
Dennis
d4f2b9839e package updates 2022-06-27 10:25:10 +02:00
Dennis
817f6a175c wip 2022-06-27 10:23:57 +02:00
Dennis
b3619e5941 wip 2022-06-20 09:14:56 +02:00
Dennis
33784410e5 wip trial 2022-06-07 13:36:49 +02:00
Dennis
6ecf7904fe wip roadmap 2022-06-07 11:31:39 +02:00
Dennis
21986f2394 add ability to run octane 2022-06-07 11:00:56 +02:00
Dennis
4d8212e56f package updates and add aws for ses emailing 2022-06-07 10:58:38 +02:00
Dennis
865f2958cf Merge branch 'develop' 2022-05-23 16:43:39 +02:00
Dennis
17890d13ad Merge branch 'develop' of https://github.com/ploi-deploy/ploi-core into develop 2022-05-23 16:43:28 +02:00
Dennis
2d33455731 Closes #1 2022-05-23 16:43:15 +02:00
Dennis
49481f9b6a Prod mix 2022-04-14 12:39:01 +02:00
Dennis
7bb800cc0a Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2022-04-14 12:38:06 +02:00
Dennis
1b8c2c764f fx 2022-04-14 12:37:39 +02:00
Dennis
cb1a1c4c06 w 2022-04-12 10:18:47 +02:00
Dennis
ddd80a8687 site email 2022-04-11 19:13:40 +02:00
Dennis
010d4569c2 package updates 2022-04-11 18:54:44 +02:00
Dennis
62ae0f8299 wip 2022-04-09 19:06:17 +02:00
Dennis
2a3d9cabd0 wip 2022-04-09 07:54:47 +02:00
Dennis
e2a58cf2df tweaks 2022-04-07 14:48:45 +02:00
Dennis
0fd6db251b remove these 2022-04-07 14:47:31 +02:00
Dennis
94d50c11ef wip 2022-04-07 14:46:56 +02:00
Dennis
14c6faafa2 wip 2022-04-07 14:21:57 +02:00
Dennis
ea21076eda wip 2022-04-07 11:15:06 +02:00
Dennis
d378323602 wip 2022-04-07 10:52:42 +02:00
Dennis
f074dee990 wip 2022-04-05 14:59:27 +02:00
Dennis
7bd2917ec4 wip 2022-04-01 10:50:05 +02:00
Dennis
823a39ffa2 prod mix 2022-03-23 09:58:27 +01:00
Dennis
6953a8d2b2 Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2022-03-23 09:58:01 +01:00
Dennis
fb40b450b6 BRL 2022-03-23 09:54:11 +01:00
Dennis
a28c053945 wip 2022-03-23 09:40:29 +01:00
Dennis
ea0c4ed66d wip 2022-03-23 09:06:28 +01:00
Dennis
7a0716959a Laravel 9 upgrade 2022-03-01 11:21:58 +01:00
Dennis
da322d7b1e prod mix 2022-01-21 13:57:39 +01:00
Dennis
bbff8a5403 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2022-01-21 13:57:04 +01:00
Dennis
c1ad600042 Ability to select database type, package updates, bugfixes 2022-01-21 13:56:48 +01:00
Dennis
5fc32759fd prod mix 2022-01-19 14:29:36 +01:00
Dennis
59cfd8d71b Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2022-01-19 14:28:00 +01:00
Dennis
51d5b89df7 wip 2022-01-19 14:26:08 +01:00
Dennis
7c0dcbeb88 TW3.0 upgrade 2021-12-24 16:30:41 +01:00
Dennis
b4467f8d5b Package updates 2021-12-21 09:04:54 +01:00
Dennis
363f4ed801 default to predis here 2021-12-21 08:58:25 +01:00
Dennis
1b6b950fb5 Bugfixing 2021-12-21 08:57:55 +01:00
Dennis
90988f1538 Prod mix 2021-12-19 21:58:01 +01:00
Dennis
139ba793d0 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-12-19 21:57:32 +01:00
Dennis
c505dd0924 PSR 2021-12-19 21:57:23 +01:00
Dennis
ca5ee33978 Bugfix in terms page, made system available in demo 2021-12-19 21:57:10 +01:00
Dennis
c2fd5e3fa9 Prod mix 2021-12-07 15:20:10 +01:00
Dennis
b04176ce48 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-12-07 15:19:25 +01:00
Dennis
5136a4b9f8 Bugfix 2021-12-07 15:18:35 +01:00
Dennis
dcee703aa1 wip 2021-11-30 13:30:21 +01:00
Dennis
7c504339d9 Added favicon if logo is uploaded 2021-11-30 13:18:10 +01:00
Dennis
5ea0761fe9 prod mix 2021-11-24 11:25:14 +01:00
Dennis
c9125c3be8 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-11-24 11:22:00 +01:00
Dennis
84503c19db Ability to revoke card & DNS fix on site show 2021-11-24 11:20:24 +01:00
Dennis
cfd9eba5d7 Ability to remove logo and pagination to system logs 2021-11-19 14:14:20 +01:00
Dennis
10689d3d12 Ability to rotate logs in system 2021-11-19 13:52:32 +01:00
Dennis
e190fb7805 prod mix 2021-11-02 11:44:32 +01:00
Dennis
c1351f7d28 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-11-02 11:43:24 +01:00
Dennis
01f5469e86 Generic fixes 2021-11-02 11:43:10 +01:00
Dennis
8c5c86eb6a Preserve scroll by default 2021-11-02 11:26:53 +01:00
Dennis
995ada46aa Prod mix 2021-09-27 09:51:05 +02:00
Dennis
2e79381872 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-09-27 09:50:24 +02:00
Dennis
d1c7b9a418 Fixes 2021-09-27 09:49:42 +02:00
Dennis
34da2f563d Ability to configure per page number 2021-09-23 14:22:01 +02:00
Dennis
5cf77fde1c Package updates 2021-09-23 08:28:54 +02:00
Dennis
aabf6f27ac Ran PSR formatter 2021-09-23 08:27:27 +02:00
Dennis
fbcaee3bdc prod mix 2021-09-23 08:26:39 +02:00
Dennis
3082c10cdb Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-09-23 08:26:03 +02:00
Dennis
a4d90f0017 OG image updatee readme 2021-09-23 08:25:52 +02:00
Dennis
8adfc9837e fixes 2021-09-23 08:20:22 +02:00
Dennis
aa20c8a42b prod mix 2021-09-16 09:51:17 +02:00
Dennis
0f100d6159 Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2021-09-16 09:50:07 +02:00
Dennis
7c896243a5 wip 2021-09-16 09:49:53 +02:00
Dennis
954fef7c3e Dynamic servers tab, fix for domain uppercase 2021-09-16 09:19:41 +02:00
Dennis
105126e498 prod mix 2021-08-25 09:20:18 +02:00
Dennis
7aa5a8949d Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-08-25 09:19:45 +02:00
Dennis
27079928a7 added demo quick-login support 2021-08-25 09:19:10 +02:00
Dennis
99968e57ec wip 2021-08-17 13:00:47 +02:00
Dennis
f75bc1a551 Production mix 2021-08-12 11:25:19 +02:00
Dennis
ef347f9381 Merge branch 'develop' 2021-08-12 11:24:30 +02:00
Dennis
f1aace3d8f Ability to select allowed regions/plans per provider 2021-08-12 11:23:01 +02:00
Dennis
b2bec62766 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-08-12 11:03:51 +02:00
Dennis
3a8682bbed Textual change 2021-08-12 11:03:25 +02:00
Dennis
5cfc1f97fe Logo fix 2021-08-12 10:26:29 +02:00
Dennis
cfc8220f8e Wip 2021-08-12 10:23:23 +02:00
Dennis
ac5ffefaed Horizon status to system settings tab 2021-08-10 14:04:52 +02:00
Dennis
cbd2b8e0e9 Wip on terms & privacy page configurator 2021-08-10 13:51:37 +02:00
Dennis
f0f427a7bb General improvements 2021-08-05 11:31:38 +02:00
Dennis
7b6f651015 Consistent Ploi class instantiate 2021-08-05 11:14:16 +02:00
Dennis
c793daa79a wip 2021-08-05 11:08:19 +02:00
Dennis
71b436aebe Production mix 2021-08-04 10:17:42 +02:00
Dennis
06e108da5b Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-08-04 10:17:03 +02:00
Dennis
5677c58dd2 Package updates 2021-08-04 10:07:36 +02:00
Dennis
58ca801e30 PSR formatting 2021-08-04 09:49:47 +02:00
Dennis
e750d7caba Cleaning 2021-08-04 09:49:24 +02:00
Dennis
3c0964ef0e Bugfixes in server settings, site creation 2021-08-04 09:46:11 +02:00
Dennis
33613cdf1c Sync all sites & servers, bugfix in pagination providers 2021-08-04 09:36:28 +02:00
Dennis
abfe174825 Autofocus on login page 2021-08-04 09:19:40 +02:00
Dennis
c750f469bb Updates
- Generic demo check in base controller
- Site synchronization
- User show pagination preserve scroll
2021-08-03 11:58:38 +02:00
Dennis
c7ac56d5cc Wip custom certificates 2021-07-29 09:59:20 +02:00
Dennis
6601f44013 wip custom certificates 2021-07-29 09:07:06 +02:00
Dennis
b7baf862b6 prod mix 2021-07-27 08:33:16 +02:00
Dennis
ca79f4cf21 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-07-27 08:32:39 +02:00
Dennis
58ffbe8c74 URL fixes 2021-07-27 08:32:30 +02:00
Dennis
dfa7b995fc wip 2021-07-23 13:46:53 +02:00
Dennis
59c65fe6ee prod 2021-07-18 10:12:47 +02:00
Dennis
087c042c14 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-07-18 10:12:15 +02:00
Dennis
d1ee488ffd fixes 2021-07-18 10:11:50 +02:00
780 changed files with 65832 additions and 28346 deletions

View File

@@ -4,10 +4,13 @@ APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_DEMO=false
APP_DATE_TIME_FORMAT="Y-m-d H:i:s"
PLOI_TOKEN=
PLOI_CORE_TOKEN=
IMPERSONATION=false
LOG_CHANNEL=stack
DB_CONNECTION=mysql
@@ -26,6 +29,7 @@ SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_CLIENT=predis
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
@@ -50,5 +54,5 @@ PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

33
.github/workflows/run-tests.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: "Run tests"
on:
push:
workflow_call:
jobs:
test:
name: Pest (PHP ${{ matrix.php }} ${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
php: [8.2]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, mysql
coverage: none
- name: Install dependencies
run: |
composer install
- name: Execute tests
run: vendor/bin/pest

8
.gitignore vendored
View File

@@ -1,8 +1,10 @@
/node_modules
/node_modules.nosync
/public/hot
/public/storage
/storage/*.key
/vendor
/vendor.nosync
.env
.env.backup
.phpunit.result.cache
@@ -12,4 +14,10 @@ npm-debug.log
yarn-error.log
.idea
.php_cs.cache
.php-cs-fixer.cache
/public/js/resources*.js
/storage/views/header.blade.php
/storage/views/footer.blade.php
rr
.rr.yaml
.DS_Store

View File

@@ -9,5 +9,5 @@ php:
js:
finder:
not-name:
- webpack.mix.js
- vite.config.js
css: true

View File

@@ -1,9 +1,9 @@
# Ploi Core
With Ploi Core, you'll power-launch your webhosting company.
With Ploi Core, you'll power-launch your webhosting company.
Using the ploi.io system as backbone you will be able to serve your customers your custom panel & feeling.
<p align="center"><img src="https://ploi-core.io/images/featured.png" width="100%"></p>
<p align="center"><img src="https://ploi-core.io/images/og.jpg" width="100%"></p>
## Documentation
@@ -17,4 +17,4 @@ https://ploi.io
The contribution guide can be found inside our documentation:
https://docs.ploi-core.io/getting-started/contribution-guide
https://docs.ploi-core.io/261-getting-started/639-contribution-guide

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Actions\Provider;
use App\Models\Provider;
use App\Services\Ploi\Ploi;
class SynchronizeProviderAction
{
public function execute(int $ploiProviderId): Provider
{
$ploiProvider = Ploi::make()->user()->serverProviders($ploiProviderId)->getData();
$provider = Provider::updateOrCreate([
'ploi_id' => $ploiProvider->id,
], [
'label' => $ploiProvider->label,
'name' => $ploiProvider->name,
]);
foreach ($ploiProvider->provider->plans as $plan) {
$provider->plans()->updateOrCreate([
'plan_id' => $plan->id,
], [
'label' => $plan->name,
]);
}
foreach ($ploiProvider->provider->regions as $region) {
$provider->regions()->updateOrCreate([
'region_id' => $region->id,
], [
'label' => $region->name,
]);
}
return $provider;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Actions\Server;
use App\Models\User;
use App\Models\Server;
use App\Jobs\Servers\CreateServer;
use Illuminate\Support\Facades\Mail;
use App\DataTransferObjects\ServerData;
use App\Mail\Admin\Server\AdminServerCreatedEmail;
class CreateServerAction
{
public function execute(ServerData $serverData): Server
{
[$provider, $providerRegion, $providerPlan] = $this->determineProviderRegionPlan($serverData);
$server = $serverData->getUser()->servers()->create([
'name' => $serverData->name,
'database_type' => $serverData->database_type,
'os' => setting('default_os', Server::OS_UBUNTU_22)
]);
$server->provider()->associate($provider);
$server->providerRegion()->associate($providerRegion);
$server->providerPlan()->associate($providerPlan);
$server->save();
dispatch(new CreateServer($server));
$this->sendAdminServerCreatedEmails($server);
return $server;
}
protected function determineProviderRegionPlan(ServerData $serverData): array
{
$provider = $serverData->getUser()->package->providers()->findOrFail($serverData->provider_id);
$region = $provider->regions()->findOrFail($serverData->provider_region_id);
$plan = $provider->plans()->findOrFail($serverData->provider_plan_id);
return [$provider, $region, $plan];
}
protected function sendAdminServerCreatedEmails(Server $server): void
{
if (! setting('receive_email_on_server_creation')) {
return;
}
$admins = User::query()->where('role', User::ADMIN)->get();
foreach ($admins as $admin) {
Mail::to($admin)->send(new AdminServerCreatedEmail(auth()->user(), $server));
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Actions\Server;
use Throwable;
use App\Models\Server;
use App\Services\Ploi\Ploi;
use Filament\Notifications\Notification;
class SynchronizeServerAction
{
public function execute(int $ploiServerId): Server|null
{
try {
$serverData = Ploi::make()->server()->get($ploiServerId)->getData();
} catch (Throwable $exception) {
Notification::make()
->title('An error has occurred: ' . $exception->getMessage())
->danger()
->send();
return null;
}
if(!$serverData){
Notification::make()
->title('Server synchronization')
->body('It was not possible to synchronize servers, it seems the API key has the wrong scopes. Please make sure the Ploi API key you\'ve entered has all the scopes enabled.')
->danger()
->send();
return null;
}
try {
$server = Server::query()
->updateOrCreate([
'ploi_id' => $serverData->id,
], [
'status' => $serverData->status,
'name' => $serverData->name,
'ip' => $serverData->ip_address,
'ssh_port' => $serverData->ssh_port,
'internal_ip' => $serverData->internal_ip,
'available_php_versions' => $serverData->installed_php_versions,
]);
} catch (Throwable $exception) {
Notification::make()
->title('An error has occurred: ' . $exception->getMessage())
->danger()
->send();
return null;
}
Notification::make()
->title(__('Server :server synchronized successfully.', ['server' => $server->name]))
->success()
->send();
return $server;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use App\Jobs\Sites\CreateSite;
use Illuminate\Support\Facades\Mail;
use App\DataTransferObjects\SiteData;
use Illuminate\Database\Eloquent\Model;
use App\Mail\Admin\Site\AdminSiteCreatedEmail;
class CreateSiteAction
{
public function execute(SiteData $siteData): ?Site
{
$server = $this->determineServer($siteData);
if (! $server) {
return null;
}
$site = $server->sites()->create($siteData->toArray());
$siteData->getUser()->sites()->save($site);
dispatch(new CreateSite($site));
$siteData->getUser()->systemLogs()->create([
'title' => 'New site :site created',
'description' => 'A new site has been created',
])->model()->associate($site)->save();
$this->sendAdminSiteCreatedEmails($server, $site, $siteData->getUser());
return $site;
}
protected function determineServer(SiteData $siteData): ?Server
{
if ($siteData->server_id) {
return $siteData->getUser()->servers()->findOrFail($siteData->server_id);
}
$server = Server::query()
->where('maximum_sites', '>', 0)
->where(function ($query) use ($siteData) {
return $query
->where(fn ($query) => $query->whereHas('users', fn ($query) => $query->where('user_id', $siteData->getUser()->id)))
->orWhere(function ($query) {
return $query->doesntHave('users');
});
})
->withCount('sites')
->inRandomOrder()
->first();
return $server && $server->sites_count < $server->maximum_sites
? $server
: null;
}
protected function sendAdminSiteCreatedEmails(Server $server, Model|Site $site, User $user): void
{
if (! setting('receive_email_on_site_creation')) {
return;
}
$admins = User::where('role', User::ADMIN)->get();
foreach ($admins as $admin) {
Mail::to($admin)->send(new AdminSiteCreatedEmail(user: $user, server: $server, site: $site));
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use App\Models\Server;
use App\Services\Ploi\Ploi;
use Filament\Notifications\Notification;
class SynchronizeSiteAction
{
public function execute(int $ploiServerId, int $ploiSiteId): Site
{
$siteData = Ploi::make()->server($ploiServerId)->sites()->get($ploiSiteId)->getData();
$server = Server::query()
->where('ploi_id', $siteData->server_id)
->firstOrFail();
$site = Site::query()
->updateOrCreate([
'ploi_id' => $siteData->id,
], [
'domain' => $siteData->domain,
'php_version' => $siteData->php_version,
'project' => $siteData->project_type,
]);
$site->status = $siteData->status;
$site->server_id = $server->id;
$site->save();
$certificates = Ploi::make()->server($siteData->server_id)->sites($siteData->id)->certificates()->get()->getData();
if ($certificates) {
foreach ($certificates as $certificate) {
$site->certificates()->updateOrCreate([
'ploi_id' => $certificate->id,
], [
'status' => $certificate->status,
'ploi_id' => $certificate->id,
'domain' => $certificate->domain,
'type' => $certificate->type,
]);
}
}
Notification::make()
->title(__('Site :site synchronized successfully.', ['site' => $site->domain]))
->success()
->send();
return $site;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Actions\User;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use App\Jobs\Sites\DeleteSite;
use App\Jobs\Servers\DeleteServer;
class DeleteUserAction
{
public function execute(User $user, bool $removeAllData): void
{
if ($removeAllData) {
$this->removeAllData($user);
}
// The next items are already being deleted by the "deleting" event:
// systemLogs, servers detached, sites detached, supportTickets, supportTicketReplies, userProviders
$user->delete();
}
protected function removeAllData(User $user): void
{
$user
->sites()
->withCount('users')
->get()
->filter(fn (Site $site) => $site->users_count === 1)
->each(function (Site $site) {
dispatch(new DeleteSite($site->server->ploi_id, $site->ploi_id));
// Deletes databases, redirects, cronjobs, certificates.
$site->delete();
});
$user
->servers()
->withCount('users')
->get()
->filter(fn (Server $server) => $server->users_count === 1)
->each(function (Server $server) {
dispatch(new DeleteServer($server->ploi_id));
// Deletes databases, redirects, cronjobs, certificates.
$server->delete();
});
}
}

26
app/Casts/SiteAlias.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class SiteAlias implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
if (!$value) {
return [];
}
$data = json_decode($value, true);
sort($data);
return $data;
}
public function set($model, string $key, $value, array $attributes)
{
return json_encode($value);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands\Core;
use App\Models\SystemLog;
use Illuminate\Console\Command;
class Cleanup extends Command
{
protected $signature = 'core:cleanup';
protected $description = 'Clean up any old logs';
public function handle()
{
if (!setting('rotate_logs_after')) {
return Command::SUCCESS;
}
$rotationDate = $this->getRotationDate();
$rotated = SystemLog::query()
->where('created_at', '<', $rotationDate)
->delete();
$this->info('Rotated ' . $rotated . ' system logs!');
return Command::SUCCESS;
}
protected function getRotationDate()
{
switch (setting('rotate_logs_after')) {
case 'weeks-1':
return now()->subWeek();
break;
case 'months-1':
return now()->subMonth();
break;
case 'months-3':
return now()->subMonths(3);
break;
case 'months-6':
return now()->subMonths(6);
break;
case 'years-1':
return now()->subYear();
break;
case 'years-2':
return now()->subYears(2);
break;
case 'years-3':
return now()->subYears(3);
break;
case 'years-4':
return now()->subYears(4);
break;
}
}
}

View File

@@ -7,6 +7,7 @@ use App\Models\User;
use RuntimeException;
use App\Models\Package;
use App\Services\Ploi\Ploi;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Console\Command;
use App\Services\VersionChecker;
@@ -41,6 +42,9 @@ class Install extends Command
$this->line(' ');
$this->writeSeparationLine();
$this->info('Make sure to also setup emailing, the cronjob and the queue worker.');
$this->line(' ');
$this->info('Setting up emailing: https://docs.ploi-core.io/261-getting-started/918-setting-up-email');
$this->info('Setting up cronjob & queue worker: https://docs.ploi-core.io/261-getting-started/638-installation');
$this->writeSeparationLine();
$this->line(' ');
$this->info('Visit your platform at ' . env('APP_URL'));
@@ -53,28 +57,32 @@ class Install extends Command
protected function askAboutAdministrationAccount()
{
$this->info('Let\'s start by setting up your administration account.');
if (!User::query()->where('role', User::ADMIN)->count()) {
$this->info('Let\'s start by setting up your administration account.');
$name = $this->ask('What is your name', $this->company['user_name']);
$email = $this->ask('What is your e-mail address', $this->company['email']);
$password = $this->secret('What password do you desire');
$name = $this->ask('What is your name', $this->company['user_name']);
$email = $this->ask('What is your e-mail address', $this->company['email']);
$password = $this->secret('What password do you desire');
$check = User::where('email', $email)->count();
$check = User::where('email', $email)->count();
if ($check) {
$this->line('');
$this->comment('This user is already present in your system, please refresh your database or use different credentials.');
$this->comment('Aborting installation..');
if ($check) {
$this->line('');
$this->comment('This user is already present in your system, please refresh your database or use different credentials.');
$this->comment('Aborting installation..');
exit();
exit();
}
User::forceCreate([
'name' => $name,
'email' => $email,
'password' => $password,
'role' => User::ADMIN
]);
} else {
$this->line('Already found a administrator user in your system. Use that user to login.');
}
User::forceCreate([
'name' => $name,
'email' => $email,
'password' => $password,
'role' => User::ADMIN
]);
}
protected function askAboutDefaultPackages()
@@ -145,7 +153,9 @@ class Install extends Command
->get((new Ploi)->url . 'ping');
if (!$response->ok() || !$response->json()) {
return false;
return [
'error' => Arr::get($response->json(), 'message', 'An unknown error has occurred.')
];
}
return $response->json();
@@ -192,9 +202,9 @@ class Install extends Command
{
if (!config('app.key')) {
$this->call('key:generate');
}
$this->info('Application key has been set');
$this->info('Application key has been set');
}
}
protected function checkApplicationUrl()
@@ -281,6 +291,18 @@ class Install extends Command
exit();
}
if (isset($this->company['error'])) {
$this->error($this->company['error']);
exit();
}
if ($this->company['user']['subscription'] !== 'unlimited') {
$this->error('Your subscription does not cover the usage of Ploi Core. Please upgrade your subscription to Unlimited.');
exit();
}
$this->writeToEnvironmentFile('PLOI_TOKEN', $ploiApiToken);
$this->writeToEnvironmentFile('PLOI_CORE_TOKEN', $ploiCoreKey);
@@ -293,7 +315,7 @@ class Install extends Command
protected function runDatabaseMigrations()
{
$this->info('Running database migrations..');
$this->call('migrate');
$this->call('migrate', ['--force' => true]);
$this->info('Database migrations successful');
}

View File

@@ -9,7 +9,7 @@ class Synchronize extends Command
{
protected $signature = 'core:synchronize';
protected $description = 'Synchronze data';
protected $description = 'Synchronize data';
public function handle()
{

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Console\Commands\Core;
use App\Models\User;
use Illuminate\Console\Command;
class Trial extends Command
{
protected $signature = 'core:trial';
protected $description = 'Check for expired trials';
public function handle()
{
User::query()
->where('trial_ends_at', '<', now())
->each(function (User $user) {
$user->trial_ends_at = null;
$user->package_id = setting('default_package');
$user->save();
});
}
}

View File

@@ -4,6 +4,8 @@ namespace App\Console;
use App\Jobs\Core\Ping;
use App\Console\Commands\Core\Css;
use App\Console\Commands\Core\Trial;
use App\Console\Commands\Core\Cleanup;
use App\Console\Commands\Core\Install;
use App\Console\Commands\Core\CssBackup;
use App\Console\Commands\Core\Synchronize;
@@ -17,6 +19,8 @@ class Kernel extends ConsoleKernel
CssBackup::class,
Install::class,
Synchronize::class,
Cleanup::class,
Trial::class,
];
protected function schedule(Schedule $schedule)
@@ -24,5 +28,8 @@ class Kernel extends ConsoleKernel
$schedule->call(function () {
dispatch(new Ping())->delay(now()->addMinutes(rand(1, 30)));
})->dailyAt('02:00');
$schedule->command('core:cleanup')->daily();
$schedule->command('core:trial')->dailyAt('10:00');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\DataTransferObjects;
use Illuminate\Support\Carbon;
use App\DataTransferObjects\Support\Data;
class PackageData extends Data
{
public function __construct(
// Add validation attributes to this class if we add additional API-endpoints.
public int $id,
public ?string $name,
public ?int $maximum_servers,
public ?int $maximum_sites,
public float $price_hourly,
public float $price_monthly,
public float $price_yearly,
public ?string $stripe_plan_id,
public string $currency,
public array $server_permissions,
public array $site_permissions,
public Carbon $created_at,
) {
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\DataTransferObjects;
use App\Models\User;
use App\Models\Server;
use App\Models\Provider;
use App\Models\ProviderPlan;
use App\Models\ProviderRegion;
use Illuminate\Support\Carbon;
use App\DataTransferObjects\Support\Data;
use Spatie\LaravelData\Attributes\Validation\In;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\NotIn;
use Spatie\LaravelData\Attributes\Validation\Exists;
use Spatie\LaravelData\Attributes\Validation\AlphaDash;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use App\DataTransferObjects\Support\Concerns\BelongsToUser;
class ServerData extends Data
{
use BelongsToUser;
public function __construct(
public ?int $id = null,
#[StringType]
public ?string $status = null,
#[StringType, AlphaDash, Max(40)]
public string $name,
#[NotIn(0), Exists(Provider::class, 'id')]
public int $provider_id,
#[NotIn(0), Exists(ProviderRegion::class, 'id')]
public int $provider_region_id,
#[NotIn(0), Exists(ProviderPlan::class, 'id')]
public int $provider_plan_id,
#[StringType, In(['mysql', 'mariadb', 'postgresql', 'postgresql13'])]
public string $database_type,
#[Exists(User::class, 'id'), IntegerType]
public ?int $user_id = null,
public ?Carbon $created_at = null,
) {
}
public static function fromModel(Server $server): static
{
return static::from(array_merge($server->toArray(), ['user_id' => $server->user->id]));
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\DataTransferObjects;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use App\Rules\Hostname;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use App\Rules\ValidateMaximumSites;
use App\DataTransferObjects\Support\Data;
use Spatie\LaravelData\Attributes\Validation\Exists;
use App\DataTransferObjects\Support\Rules\CustomRule;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use App\DataTransferObjects\Support\Concerns\BelongsToUser;
class SiteData extends Data
{
use BelongsToUser;
public function __construct(
public ?int $id = null,
public ?string $status = null,
#[Exists(Server::class, 'id'), IntegerType]
public ?int $server_id = null,
#[StringType, CustomRule(Hostname::class, ValidateMaximumSites::class)]
public ?string $domain = null,
#[Exists(User::class, 'id'), IntegerType]
public ?int $user_id = null,
public ?Carbon $created_at = null,
) {
}
public static function authorize(): bool
{
if (auth()->guest()) {
return true;
}
return auth()->user()->can('create', Site::class);
}
public static function fromModel(Site $site): static
{
return static::from(array_merge($site->toArray(), ['user_id' => $site->user->id]));
}
public function toArray(): array
{
return Arr::only(parent::toArray(), [
'id',
'status',
'server_id',
'domain',
'user_id',
'created_at',
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\DataTransferObjects\Support\Casts;
use Illuminate\Support\Carbon;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Support\DataProperty;
class CarbonCast implements Cast
{
public function cast(DataProperty $property, mixed $value, array $context): Carbon
{
return Carbon::parse($value);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\DataTransferObjects\Support\Concerns;
use App\Models\User;
trait BelongsToUser
{
public function getUser(): ?User
{
return User::find($this->user_id);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\DataTransferObjects\Support;
class Data extends \Spatie\LaravelData\Data
{
/**
* When working with paginated data, we want to include pagination details in JSON
* responses from the API. However, due to legacy requirements Ploi Core is using
* a different structure than this package assumes. Therefore, we will override
* the data collection, register a custom transformer and output the structure.
*/
protected static string $_paginatedCollectionClass = PaginatedDataCollection::class;
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\DataTransferObjects\Support;
class DataCollectableTransformer extends \Spatie\LaravelData\Transformers\DataCollectableTransformer
{
protected function wrapPaginatedArray(array $paginated): array
{
$wrapKey = $this->wrap->getKey() ?? 'data';
return [
$wrapKey => $paginated['data'],
'links' => [
'first' => $paginated['first_page_url'],
'last' => $paginated['last_page_url'],
'prev' => $paginated['prev_page_url'],
'next' => $paginated['next_page_url'],
],
'meta' => [
'current_page' => $paginated['current_page'],
'from' => $paginated['from'],
'last_page' => $paginated['last_page'],
'links' => $paginated['links'],
'path' => $paginated['path'],
'per_page' => $paginated['per_page'],
'to' => $paginated['to'],
'total' => $paginated['total'],
],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\DataTransferObjects\Support;
use Spatie\LaravelData\Support\Wrapping\WrapExecutionType;
class PaginatedDataCollection extends \Spatie\LaravelData\PaginatedDataCollection
{
public function transform(bool $transformValues = true, WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, bool $mapPropertyNames = true): array
{
$transformer = new DataCollectableTransformer(
$this->dataClass,
$transformValues,
$wrapExecutionType,
$mapPropertyNames,
$this->getPartialTrees(),
$this->items,
$this->getWrap(),
);
return $transformer->transform();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\DataTransferObjects\Support\Rules;
use Attribute;
use Spatie\LaravelData\Support\Validation\ValidationPath;
use Spatie\LaravelData\Attributes\Validation\CustomValidationAttribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class CustomRule extends CustomValidationAttribute
{
protected array $rules = [];
public function __construct(...$rules)
{
$this->rules = $rules;
}
/**
* @return array<object|string>|object|string
*/
public function getRules(ValidationPath $path): array|object|string
{
return array_map(
fn (string $ruleClass) => new $ruleClass(),
$this->rules
);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\DataTransferObjects\Support\Transformers;
use Illuminate\Support\Carbon;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Transformers\Transformer;
class CarbonTransformer implements Transformer
{
public function transform(DataProperty $property, mixed $value): string
{
/** @var Carbon $value */
return $value->toISOString();
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\DataTransferObjects;
use App\Models\User;
use App\Models\Package;
use Illuminate\Support\Carbon;
use App\DataTransferObjects\Support\Data;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Email;
use Spatie\LaravelData\Attributes\Validation\Exists;
use Spatie\LaravelData\Attributes\Validation\Unique;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
class UserData extends Data
{
public function __construct(
public ?int $id = null,
public ?string $avatar = null,
#[StringType, Max(255)]
public ?string $name = null,
#[StringType, Email, Max(255), Unique(User::class)]
public ?string $email = null,
#[Exists(Package::class, 'id'), IntegerType]
public ?int $package_id = null,
#[StringType]
public ?string $blocked = null,
#[StringType]
public ?string $language = 'en',
#[BooleanType]
public ?bool $requires_password_for_ftp = true,
public ?Carbon $created_at = null,
) {
}
}

View File

@@ -2,7 +2,12 @@
namespace App\Exceptions;
use Exception;
use Throwable;
use Illuminate\Http\Request;
use App\Http\Middleware\SetLocale;
use App\Http\Middleware\HandleInertiaRequests;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
@@ -29,10 +34,10 @@ class Handler extends ExceptionHandler
/**
* Report or log an exception.
*
* @param \Throwable $exception
* @param Throwable $exception
* @return void
*
* @throws \Exception
* @throws Exception
*/
public function report(Throwable $exception)
{
@@ -42,24 +47,25 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
* @param Request $request
* @param Throwable $exception
* @return Response
*
* @throws \Throwable
* @throws Throwable
*/
public function render($request, Throwable $exception)
{
$response = parent::render($request, $exception);
if (in_array($response->status(), [404, 403])) {
\Route::any($request->path(), function () use ($exception, $request) {
return parent::render($request, $exception);
})->middleware('web');
// Only return an Inertia-response when there are special Vue-templates (403 and 404) and when it isn't an API request.
if (in_array($response->status(), [403, 404]) && ! $request->routeIs('api.*')) {
inertia()->share([
'translations' => SetLocale::getTranslations()
]);
return inertia()->render('Errors/' . $response->status(), ['status' => $response->status()])
->toResponse($request)
->setStatusCode($response->status());
return app(HandleInertiaRequests::class)
->handle($request, fn () => inertia()->render('Errors/' . $response->status(), ['status' => $response->status()])
->toResponse($request));
}
return $response;

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Filament\Pages;
use Filament\Forms;
use App\Models\Server;
use App\Models\Package;
use Filament\Pages\Page;
use Illuminate\Support\Str;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Facades\Storage;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Forms\Components\Actions\Action;
class Settings extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-cog';
protected static string $view = 'filament.pages.settings';
protected static ?string $navigationGroup = 'Settings';
protected static ?int $navigationSort = 1;
public array $data = [];
public function mount(): void
{
cache()->forget('core.settings');
$this->form->fill([
'logo' => setting('logo'),
'name' => setting('name'),
'email' => setting('email'),
'support_emails' => setting('support_emails'),
'default_package' => setting('default_package'),
'default_language' => setting('default_language'),
'rotate_logs_after' => setting('rotate_logs_after'),
'trial' => (bool)setting('trial'),
'support' => (bool)setting('support'),
'documentation' => (bool)setting('documentation'),
'allow_registration' => (bool)setting('allow_registration'),
'receive_email_on_server_creation' => (bool)setting('receive_email_on_server_creation'),
'receive_email_on_site_creation' => (bool)setting('receive_email_on_site_creation'),
'enable_api' => (bool)setting('enable_api'),
'api_token' => setting('api_token'),
'isolate_per_site_per_user' => (bool)setting('isolate_per_site_per_user'),
'default_os' => setting('default_os', Server::OS_UBUNTU_22),
]);
}
public function getFormSchema(): array
{
return [
Forms\Components\Grid::make(2)
->schema([
Forms\Components\Grid::make(2)
->schema([
Forms\Components\TextInput::make('name')
->label(__('Company name'))
->required(),
Forms\Components\TextInput::make('email')
->label(__('E-mail address'))
->email(),
Forms\Components\TextInput::make('support_emails')
->label(__('Support email address'))
->helperText('Separate by comma to allow more email addresses'),
])
->columnSpan(2),
Forms\Components\Select::make('default_package')
->options(fn () => Package::orderBy('name')->pluck('name', 'id'))
->label(__('Select default package'))
->helperText(__('Select the default package a user should get when you create or they register')),
Forms\Components\Select::make('default_language')
->options(collect(languages())->mapWithKeys(fn (string $language) => [$language => $language]))
->label('Select default language')
->helperText('Select the default language a user should get when you create or they register'),
Forms\Components\FileUpload::make('logo')
->label(__('Logo'))
->disk('logos')
->columnSpan(2),
Forms\Components\Select::make('rotate_logs_after')
->label(__('This will rotate any logs older than selected, this helps cleanup your database'))
->options([
null => __("Don't rotate logs"),
'weeks-1' => __('Older than 1 week'),
'months-1' => __('Older than 1 month'),
'months-3' => __('Older than 3 months'),
'months-6' => __('Older than 6 months'),
'years-1' => __('Older than 1 year'),
'years-2' => __('Older than 2 years'),
'years-3' => __('Older than 3 years'),
'years-4' => __('Older than 4 years'),
])
->columnSpan(1),
Forms\Components\Select::make('default_os')
->label(__('Select the default OS that should be used when users create a server'))
->default(Server::OS_UBUNTU_22)
->options([
Server::OS_UBUNTU_18 => __('Ubuntu 18'),
Server::OS_UBUNTU_20 => __('Ubuntu 20'),
Server::OS_UBUNTU_22 => __('Ubuntu 22'),
])
->columnSpan(1),
Forms\Components\Toggle::make('trial')
->label(__('Enable trial'))
->helperText(__('This will allow you to have users with trials.')),
Forms\Components\Toggle::make('allow_registration')
->label(__('Allow registration'))
->helperText(__('Allow customer registration')),
Forms\Components\Toggle::make('support')
->label(__('Enable support platform'))
->helperText(__('This will allow your customers to make support requests to you.')),
Forms\Components\Toggle::make('documentation')
->label(__('Enable documentation platform'))
->helperText(__('This will allow you to create articles for your users to look at.')),
Forms\Components\Toggle::make('receive_email_on_server_creation')
->label(__('Receive email when customers create server'))
->helperText(__('This will send an email to all admins notifying them about a new server installation.')),
Forms\Components\Toggle::make('receive_email_on_site_creation')
->label(__('Receive email when customers create site'))
->helperText(__('This will send an email to all admins notifying them about a new site installation.')),
Forms\Components\Toggle::make('enable_api')
->label(__('Enable API'))
->helperText(new HtmlString(__('This will allow you to interact with your system via the API. ') . '<a href="https://docs.ploi-core.io/304-core-api/737-introduction" target="_blank" class="text-primary-600">' . __('More information') . '</a>')),
Forms\Components\TextInput::make('api_token')
->label(__('API token'))
->afterStateHydrated(function (?string $state, Forms\Components\TextInput $component) {
$state = filled($state) ? decrypt($state) : null;
$component->state($state);
})
->dehydrateStateUsing(function (?string $state) {
return filled($state) ? encrypt($state) : null;
})
->registerActions([
'generate' => $generateAction = Action::make('generate')
->label(__('Generate'))
->icon('heroicon-o-key')
->action(function (TextInput $component) {
$component->state(Str::random(20));
})
->tooltip('Generate'),
])
->suffixAction($generateAction),
Forms\Components\Toggle::make('isolate_per_site_per_user')
->label(__('Enable site isolation per site & user'))
->helperText(__('This will make sure each site created by one user is always isolated from another.')),
]),
];
}
public function getFormStatePath(): ?string
{
return 'data';
}
public function save(): void
{
$state = $this->form->getState();
$oldLogo = setting('logo');
$oldDocumentation = setting('documentation');
$oldSupport = setting('support');
if ($state['logo'] === null && $oldLogo) {
Storage::disk('logos')->delete($oldLogo);
}
setting($state);
cache()->forget('core.settings');
Notification::make()
->success()
->title(__('Settings saved.'))
->send();
if ($state['logo'] !== $oldLogo || $state['documentation'] !== $oldDocumentation || $state['support'] !== $oldSupport) {
$this->redirect(Settings::getUrl());
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Pages;
use Filament\Pages\Page;
use App\Services\VersionChecker;
use Filament\Notifications\Notification;
use Laravel\Horizon\Contracts\MasterSupervisorRepository;
class System extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-adjustments-vertical';
protected static string $view = 'filament.pages.system';
protected static ?string $navigationGroup = 'Settings';
protected static ?int $navigationSort = 2;
public function getCurrentVersion(): string
{
return app(VersionChecker::class)->getVersions()->currentVersion;
}
public function getRemoteVersion(): string
{
return app(VersionChecker::class)->getVersions()->remoteVersion;
}
public function refreshRemoteVersion(): void
{
app(VersionChecker::class)->flushVersionData();
Notification::make()
->success()
->title(__('Refreshed versions'))
->send();
}
public function getHorizonWorkerStatus(): bool
{
return rescue(fn () => (bool)app(MasterSupervisorRepository::class)->all(), false, false);
}
public function hasAvailableUpdate(): bool
{
return app(VersionChecker::class)->getVersions()->isOutOfDate();
}
public static function getNavigationBadge(): ?string
{
$systemChecker = app(VersionChecker::class);
if ($systemChecker->isOutOfDate()) {
return 'Update available';
}
return null;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Filament\Pages;
use Filament\Forms;
use Filament\Actions;
use Filament\Pages\Page;
use Illuminate\Support\Str;
use Filament\Notifications\Notification;
class Terms extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-document-text';
protected static string $view = 'filament.pages.terms';
protected static ?string $navigationGroup = 'Settings';
protected static ?int $navigationSort = 3;
public array $data = [];
public function mount(): void
{
cache()->forget('core.settings');
$this->form->fill([
'accept_terms_required' => (bool)setting('accept_terms_required'),
'terms' => setting('terms'),
'privacy' => setting('privacy'),
]);
}
protected function getFormSchema(): array
{
return [
Forms\Components\Toggle::make('accept_terms_required')
->label(__(' Require users to accept terms of service on registration'))
->helperText(__('This will require newly registered users to accept the terms of service.')),
Forms\Components\MarkdownEditor::make('terms')
->label(__('Content Terms Of Service')),
Forms\Components\MarkdownEditor::make('privacy')
->label(__('Content Privacy Policy')),
];
}
protected function getHeaderActions(): array
{
return [
Actions\Action::make('load_terms_template')
->label(__('Load Terms Of Service Template'))
->action(function (self $livewire) {
$template = Str::of(file_get_contents(storage_path('templates/terms-of-service.md')))
->replace([
'{NAME}',
'{WEBSITE}',
'{DATE}',
], [
setting('name'),
config('app.url'),
date('Y-m-d'),
])
->value();
$livewire->data['terms'] = $template;
Notification::make()
->success()
->title(__('Loaded Terms Of Service Template'))
->send();
}),
];
}
protected function getFormStatePath(): ?string
{
return 'data';
}
public function save(): void
{
$state = $this->form->getState();
setting($state);
cache()->forget('core.settings');
Notification::make()
->success()
->title(__('Terms saved.'))
->send();
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use App\Models\Alert;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use App\Filament\Resources\AlertResource\Pages;
class AlertResource extends Resource
{
protected static ?string $model = Alert::class;
protected static ?string $navigationIcon = 'heroicon-o-bell';
protected static ?string $navigationGroup = 'Settings';
protected static ?int $navigationSort = 4;
protected static ?string $recordTitleAttribute = 'message';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\MarkdownEditor::make('message')
->label(__('Content'))
->columnSpan(2)
->required(),
Forms\Components\Select::make('type')
->label(__('Type'))
->options([
Alert::TYPE_INFO => __('Informational'),
Alert::TYPE_WARNING => __('Warning'),
Alert::TYPE_DANGER => __('Danger'),
])
->required(),
Forms\Components\DateTimePicker::make('expires_at')
->label(__('Expires at'))
->seconds(false),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('message')
->label(__('Content'))
->formatStateUsing(fn (?string $state) => new HtmlString(Str::markdown($state))),
Tables\Columns\TextColumn::make('type')
->label(__('Type'))
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Alert::TYPE_INFO => __('Informational'),
Alert::TYPE_WARNING => __('Warning'),
Alert::TYPE_DANGER => __('Danger'),
})
->colors([
'primary' => Alert::TYPE_INFO,
'warning' => Alert::TYPE_WARNING,
'danger' => Alert::TYPE_DANGER,
]),
Tables\Columns\TextColumn::make('expires_at')
->label('Expires')
->default('-'),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListAlerts::route('/'),
'create' => Pages\CreateAlert::route('/create'),
'edit' => Pages\EditAlert::route('/{record}/edit'),
];
}
public static function getGloballySearchableAttributes(): array
{
return [];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\AlertResource\Pages;
use App\Filament\Resources\AlertResource;
use Filament\Resources\Pages\CreateRecord;
class CreateAlert extends CreateRecord
{
protected static string $resource = AlertResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\AlertResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\AlertResource;
class EditAlert extends EditRecord
{
protected static string $resource = AlertResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Filament\Resources\AlertResource\Pages;
use Filament\Actions;
use App\Filament\Resources\AlertResource;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Contracts\Support\Htmlable;
class ListAlerts extends ListRecords
{
protected static string $resource = AlertResource::class;
protected ?string $subheading = 'Alerts are meant to inform your users about things that are going on. For example server migrations, pricing changes. They will display as top-banner inside the panel.';
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Models\Certificate;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use Illuminate\Database\Eloquent\Builder;
use App\Filament\Resources\CertificateResource\Pages;
class CertificateResource extends Resource
{
protected static ?string $model = Certificate::class;
protected static ?string $navigationIcon = 'heroicon-o-chat-bubble-bottom-center-text';
protected static ?string $navigationGroup = 'Site management';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('site.name'),
Forms\Components\TextInput::make('server_id'),
Forms\Components\TextInput::make('status'),
Forms\Components\TextInput::make('ploi_id'),
Forms\Components\TextInput::make('domain'),
Forms\Components\Textarea::make('certificate'),
Forms\Components\Textarea::make('private'),
Forms\Components\TextInput::make('type'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('server.name')
->searchable()
->label(__('Server')),
Tables\Columns\TextColumn::make('site.domain')
->searchable()
->label(__('Main domain')),
Tables\Columns\TextColumn::make('type')
->label('Type'),
Tables\Columns\TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Certificate::STATUS_BUSY => __('Busy'),
Certificate::STATUS_ACTIVE => __('Active'),
})
->colors([
'warning' => Certificate::STATUS_BUSY,
'success' => Certificate::STATUS_ACTIVE,
])
->label(__('Status')),
Tables\Columns\TextColumn::make('domain')
->searchable()
->wrap()
->getStateUsing(function (Certificate $record) {
$state = str($record->domain)->explode(',')->implode(', ');
return new HtmlString($state);
})
->label('Domains & aliases'),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
])
->defaultSort('created_at', 'desc');
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->orderBy('domain');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListCertificates::route('/'),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\CertificateResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\CertificateResource;
class ListCertificates extends ListRecords
{
protected static string $resource = CertificateResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Filament\Resources;
use Filament\Tables;
use App\Models\Cronjob;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Filament\Resources\CronjobResource\Pages;
class CronjobResource extends Resource
{
protected static ?string $model = Cronjob::class;
protected static ?string $navigationIcon = 'heroicon-o-clock';
protected static ?string $navigationGroup = 'Site management';
protected static ?int $navigationSort = 3;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('site.domain')
->searchable()
->label(__('Site')),
Tables\Columns\TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Cronjob::STATUS_BUSY => __('Busy'),
Cronjob::STATUS_ACTIVE => __('Active'),
})
->colors([
'warning' => Cronjob::STATUS_BUSY,
'success' => Cronjob::STATUS_ACTIVE,
])
->label(__('Status')),
Tables\Columns\TextColumn::make('server.name')
->searchable()
->label(__('Server')),
Tables\Columns\TextColumn::make('command')
->searchable()
->label(__('Command')),
Tables\Columns\TextColumn::make('frequency')
->label(__('Frequency')),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->actions([
//
])
->bulkActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListCronjobs::route('/'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\CronjobResource\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\CronjobResource;
class CreateCronjob extends CreateRecord
{
protected static string $resource = CronjobResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\CronjobResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\CronjobResource;
class EditCronjob extends EditRecord
{
protected static string $resource = CronjobResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\CronjobResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\CronjobResource;
class ListCronjobs extends ListRecords
{
protected static string $resource = CronjobResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Filament\Resources;
use Filament\Tables;
use App\Models\Database;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Filament\Resources\DatabaseResource\Pages;
class DatabaseResource extends Resource
{
protected static ?string $model = Database::class;
protected static ?string $navigationIcon = 'heroicon-o-circle-stack';
protected static ?string $navigationGroup = 'Site management';
protected static ?int $navigationSort = 4;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label(__('Name'))
->searchable(),
Tables\Columns\TextColumn::make('server.name')
->label(__('Server'))
->searchable(),
Tables\Columns\TextColumn::make('site.domain')
->label(__('Site'))
->searchable(),
Tables\Columns\TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Database::STATUS_BUSY => __('Busy'),
Database::STATUS_ACTIVE => __('Active'),
})
->colors([
'warning' => Database::STATUS_BUSY,
'success' => Database::STATUS_ACTIVE,
])
->label(__('Status')),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListDatabases::route('/'),
'edit' => Pages\EditDatabase::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Filament\Resources\DatabaseResource\Pages;
use App\Models\Database;
use App\Services\Ploi\Ploi;
use Filament\Resources\Pages\Page;
use App\Mail\Database\PasswordReset;
use Illuminate\Support\Facades\Mail;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use App\Filament\Resources\DatabaseResource;
class EditDatabase extends Page
{
protected $listeners = [
'$refresh',
];
protected static string $resource = DatabaseResource::class;
protected static string $view = 'filament.resources.database-resource.pages.edit-database';
public Database $record;
protected ?string $recentlyUpdatedPassword = null;
public function mount(): void
{
$this->resetDatabasePasswordForm->fill();
}
public function getForms(): array
{
return [
'resetDatabasePasswordForm' => $this->makeForm()
->schema([
Toggle::make('send_new_password_to_user')
->label(__('Email new password to user')),
])
->model($this->record),
];
}
public function resetDatabasePassword(): void
{
$state = $this->resetDatabasePasswordForm->getState();
$data = Ploi::make()
->server($this->record->server->ploi_id)
->databases($this->record->ploi_id)
->users($this->record->users->first()->ploi_id)
->resetPassword()
->getData();
$this->recentlyUpdatedPassword = $data->new_password;
Notification::make()
->title(__('Successfully reset database password.'))
->success()
->send();
if ($state['send_new_password_to_user']) {
Mail::to($this->record->site->users)->send(new PasswordReset($this->record, $this->recentlyUpdatedPassword));
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DatabaseResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\DatabaseResource;
class ListDatabases extends ListRecords
{
protected static string $resource = DatabaseResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use App\Models\DocumentationCategory;
use App\Filament\Resources\DocumentationCategoryResource\Pages;
use App\Filament\Resources\DocumentationCategoryResource\RelationManagers;
class DocumentationCategoryResource extends Resource
{
protected static ?string $model = DocumentationCategory::class;
protected static ?string $navigationIcon = 'heroicon-o-tag';
protected static ?string $navigationGroup = 'Documentation';
protected static ?int $navigationSort = 2;
protected static ?string $pluralLabel = 'Categories';
protected static ?string $label = 'Category';
public static function shouldRegisterNavigation(): bool
{
return (bool)setting('documentation');
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->label(__('Title'))
->unique(table: DocumentationCategory::class, column: 'title', ignoreRecord: true)
->required()
->columnSpan(2),
Forms\Components\MarkdownEditor::make('description')
->label(__('Description'))
->required()
->columnSpan(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable()
->sortable()
->label(__('Title')),
Tables\Columns\TextColumn::make('description')
->label(__('Description'))
->formatStateUsing(fn (string $state) => new HtmlString(Str::markdown($state))),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\DocumentationItemsRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListDocumentationCategories::route('/'),
'create' => Pages\CreateDocumentationCategory::route('/create'),
'edit' => Pages\EditDocumentationCategory::route('/{record}/edit'),
];
}
public static function getGloballySearchableAttributes(): array
{
return ['title'];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\DocumentationCategoryResource\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\DocumentationCategoryResource;
class CreateDocumentationCategory extends CreateRecord
{
protected static string $resource = DocumentationCategoryResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DocumentationCategoryResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\DocumentationCategoryResource;
class EditDocumentationCategory extends EditRecord
{
protected static string $resource = DocumentationCategoryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DocumentationCategoryResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\DocumentationCategoryResource;
class ListDocumentationCategories extends ListRecords
{
protected static string $resource = DocumentationCategoryResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Filament\Resources\DocumentationCategoryResource\RelationManagers;
use Filament\Forms;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\RelationManagers\RelationManager;
class DocumentationItemsRelationManager extends RelationManager
{
protected static string $relationship = 'items';
protected static ?string $recordTitleAttribute = 'title';
protected static ?string $label = 'Article';
protected static ?string $pluralLabel = 'Articles';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title'),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Models\DocumentationItem;
use App\Filament\Resources\DocumentationItemResource\Pages;
class DocumentationItemResource extends Resource
{
protected static ?string $model = DocumentationItem::class;
protected static ?string $navigationIcon = 'heroicon-o-document-duplicate';
protected static ?string $navigationGroup = 'Documentation';
protected static ?int $navigationSort = 1;
protected static ?string $pluralLabel = 'Articles';
protected static ?string $label = 'Article';
public static function shouldRegisterNavigation(): bool
{
return (bool)setting('documentation');
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->label(__('Title'))
->required(),
Forms\Components\Select::make('documentation_category_id')
->relationship('category', 'title')
->searchable()
->preload(),
Forms\Components\MarkdownEditor::make('content')
->label(__('Content'))
->required()
->columnSpan(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('category.title')
->searchable()
->sortable(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListDocumentationItems::route('/'),
'create' => Pages\CreateDocumentationItem::route('/create'),
'edit' => Pages\EditDocumentationItem::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\DocumentationItemResource\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\DocumentationItemResource;
class CreateDocumentationItem extends CreateRecord
{
protected static string $resource = DocumentationItemResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DocumentationItemResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\DocumentationItemResource;
class EditDocumentationItem extends EditRecord
{
protected static string $resource = DocumentationItemResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\DocumentationItemResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\DocumentationItemResource;
class ListDocumentationItems extends ListRecords
{
protected static string $resource = DocumentationItemResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,256 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\PackageResource\Pages;
use App\Filament\Resources\PackageResource\RelationManagers;
use App\Models\Package;
use App\Models\Provider;
use App\Models\ProviderPlan;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\HtmlString;
class PackageResource extends Resource
{
protected static ?string $model = Package::class;
protected static ?string $navigationIcon = 'heroicon-o-swatch';
protected static ?int $navigationSort = 3;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->label(__('Name'))
->required()
->columnSpan(2),
Forms\Components\TextInput::make('maximum_sites')
->helperText(__('Set to 0 for unlimited'))
->integer()
->required(),
Forms\Components\TextInput::make('maximum_servers')
->helperText(__('Set to 0 for unlimited'))
->integer()
->required(),
Forms\Components\TextInput::make('stripe_plan_id')
->helperText(new HtmlString(__('Enter the pricing ID from Stripe here') . ' - <a href="https://docs.ploi-core.io/263-digging-deeper/743-using-stripe" target="ploi-docs-stripe" class="text-primary-500">How does this work?</a>'))
->label(__('Stripe ID'))
->columnSpan(2),
Forms\Components\TextInput::make('price_monthly')
->label(__('Monthly price'))
->helperText(__('Fill this in if you want it to be monthly payments'))
->required(),
Forms\Components\TextInput::make('price_yearly')
->label(__('Yearly price'))
->helperText(__('Fill this in if you want it to be yearly payments'))
->required(),
Forms\Components\Select::make('currency')
->label(__('Currency'))
->options([
'usd' => 'USD $',
'eur' => 'Euro €',
'gbp' => 'GBP £',
'nok' => 'NOK (Norwegian Krone)',
'aud' => 'AUD (Australian dollar)',
'cad' => 'CAD (Canadian dollar)',
'inr' => 'INR (Indian ₹ rupee)',
'thb' => 'THB (Thai Baht)',
'brl' => 'BRL R$ (Brazilian Real)',
'nz' => 'NZD $ (New Zealand Dollar)',
])
->required(),
Forms\Components\Grid::make()
->schema([
Forms\Components\Section::make(__('Server permissions'))
->icon(ServerResource::getNavigationIcon())
->schema([
Forms\Components\Checkbox::make('server_permissions.create')
->reactive()
->label('Allow server creation')
->helperText('This will allow users to create servers'),
Forms\Components\Checkbox::make('server_permissions.update')
->label('Allow server updates')
->helperText('This will allow users to update servers'),
Forms\Components\Checkbox::make('server_permissions.delete')
->label('Allow server deletion')
->helperText('This will allow users to delete servers'),
])
->columnSpan(1),
Forms\Components\Section::make(__('Site permissions'))
->icon(SiteResource::getNavigationIcon())
->schema([
Forms\Components\Checkbox::make('site_permissions.create')
->label('Allow site creation')
->helperText('This will allow users to create sites'),
Forms\Components\Checkbox::make('site_permissions.update')
->label('Allow site updates')
->helperText('This will allow users to update sites'),
Forms\Components\Checkbox::make('site_permissions.delete')
->label('Allow site deletion')
->helperText('This will allow users to delete sites'),
])
->columnSpan(1),
]),
Forms\Components\Grid::make()
->schema([
Forms\Components\Section::make(__('Available server providers'))
->description(__('These server providers will be available for users that are attached to this package.'))
->icon(ProviderResource::getNavigationIcon())
->schema([
Forms\Components\CheckboxList::make('providers')
->relationship('providers', 'name')
->reactive(),
Forms\Components\Grid::make(1)
->schema([
Forms\Components\Actions::make([
Forms\Components\Actions\Action::make('manage_provider_plans')
->label(__('Manage provider plans'))
->icon('heroicon-o-adjustments-horizontal')
->form(function (Package $record) {
return $record->providers->sortBy('name')->map(function (Provider $provider) {
return Forms\Components\Section::make($provider->label)
->description(__('Select the plans that should be available for this provider on this package.'))
->icon(ProviderResource::getNavigationIcon())
->statePath($provider->id)
->schema([
Forms\Components\Toggle::make('select_specific_provider_plans')
->label(__('Select subset'))
->helperText(__('Check this box if you want to limit the provider plans available on this package.'))
->default(false)
->reactive()
->afterStateUpdated(function (Forms\Components\Toggle $component, Forms\Set $set) use ($provider) {
$set(
path: "provider_plans",
state: $component->getState() ? $provider->plans->pluck('id') : [],
);
}),
Forms\Components\CheckboxList::make("provider_plans")
->label(__('Select plans'))
->options(fn() => $provider->plans->mapWithKeys(fn(ProviderPlan $providerPlan) => [$providerPlan->id => $providerPlan->label ?? $providerPlan->plan_id])->all())
->visible(fn(Forms\Get $get) => $get('select_specific_provider_plans'))
->reactive()
->bulkToggleable()
->columns(2)
])
->collapsible();
})->all();
})
->fillForm(function (Package $record) {
return $record->providers->mapWithKeys(function (Provider $provider) use ($record) {
$providerPlanIds = $record->providerPlans()->whereBelongsTo($provider)->pluck('provider_plans.id');
return [$provider->id => [
'select_specific_provider_plans' => $providerPlanIds->isNotEmpty(),
'provider_plans' => $providerPlanIds->all(),
]];
})->all();
})
->action(function (Package $record, array $data) {
$providerPlanIds = collect($data)
// If `select_specific_provider_plans`, all provider plans are available. It could be that this
// option was deselected, and that we have some left over provider plans in the field that
// is now hidden. We will not include theSE IDs so that they ARE detached automatically.
->where('select_specific_provider_plans', true)
->pluck('provider_plans')
->flatten();
// Detaches provider plans not specifically selected.
$record->providerPlans()->sync($providerPlanIds);
Notification::make()
->title(__('Provider plans saved'))
->success()
->send();
})
->modalSubmitActionLabel(__('Save'))
->color('gray')
->disabled(function (Package $record, Forms\Get $get) {
$providers = collect($get('providers'))
->map(fn(string $id): int => (int)$id)
->sort();
return $record->providers->pluck('id')->map(fn(string $id): int => (int)$id)->sort()->toArray() !== $providers->all();
})
]),
Forms\Components\Placeholder::make('save_warning')
->content(__('You\'ve changed the available server providers. Please save your changes before you can manage the provider plans.'))
->visible(function (Package $record, Forms\Get $get) {
$providers = collect($get('providers'))
->map(fn(string $id): int => (int)$id)
->sort();
return $record->providers->pluck('id')->map(fn(string $id): int => (int)$id)->sort()->toArray() !== $providers->all();
})
->hiddenLabel(),
])
->hiddenOn('create'),
])
->columnSpan(1)
])
->visible(function ($get) {
return $get('server_permissions')['create'];
})
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('id')
->label('ID')
->searchable(),
Tables\Columns\TextColumn::make('name')
->label(__('Name'))
->description(function (Package $record) {
if (!$record->stripe_plan_id) {
return __('Not attached to Stripe.');
}
return "Attached to stripe - {$record->price_monthly} {$record->currency}";
}),
Tables\Columns\TextColumn::make('maximum_sites')
->formatStateUsing(fn(int $state) => $state === 0 ? __('Unlimited') : $state)
->label(__('Maximum sites')),
Tables\Columns\TextColumn::make('maximum_servers')
->formatStateUsing(fn(int $state) => $state === 0 ? __('Unlimited') : $state)
->label(__('Maximum servers')),
Tables\Columns\TextColumn::make('users_count')
->counts('users'),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\UsersRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListPackages::route('/'),
'create' => Pages\CreatePackage::route('/create'),
'edit' => Pages\EditPackage::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\PackageResource\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\PackageResource;
class CreatePackage extends CreateRecord
{
protected static string $resource = PackageResource::class;
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Filament\Resources\PackageResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\PackageResource;
class EditPackage extends EditRecord
{
protected static string $resource = PackageResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
public function afterSave(): void
{
// Necessary to refresh, in order to load the updated saved relationships and
// correctly show or hide the "manage provider plans" warning placeholder.
$this->getRecord()->refresh();
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\PackageResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\PackageResource;
class ListPackages extends ListRecords
{
protected static string $resource = PackageResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Filament\Resources\PackageResource\RelationManagers;
use App\Models\User;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Tables\Actions\Action;
use Filament\Forms\Components\Select;
use App\Filament\Resources\UserResource;
use Filament\Resources\RelationManagers\RelationManager;
class UsersRelationManager extends RelationManager
{
protected static string $relationship = 'users';
protected static ?string $recordTitleAttribute = 'name';
public function form(Form $form): Form
{
return UserResource::form($form);
}
public function table(Table $table): Table
{
return UserResource::table($table)
->headerActions([
...$table->getHeaderActions(),
Action::make('add_user')
->label(__('Add user'))
->form(fn (self $livewire) => [
Select::make('user_id')
->label('User')
->options(User::orderBy('name')->get()->mapWithKeys(fn (User $user) => [$user->id => $user->name]))
->preload()
->searchable()
->required(),
])
->action(function (array $data, self $livewire) {
$user = User::find($data['user_id']);
$user->update([
'package_id' => $livewire->ownerRecord->id,
]);
})
->button(),
]);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use App\Models\Provider;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Models\ProviderPlan;
use Filament\Resources\Resource;
use App\Filament\Resources\ProviderPlanResource\Pages;
class ProviderPlanResource extends Resource
{
protected static ?string $model = ProviderPlan::class;
protected static ?string $navigationIcon = 'heroicon-o-cube';
protected static ?string $navigationGroup = 'Providers';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('label'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('id')
->label('ID')
->searchable(),
Tables\Columns\TextColumn::make('provider.name')
->label(__('Provider'))
->searchable(),
Tables\Columns\TextColumn::make('plan_id')
->label(__('Plan ID'))
->searchable(),
Tables\Columns\TextColumn::make('label')
->label(__('Label'))
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
Tables\Filters\SelectFilter::make('provider_id')
->label(__('Provider'))
->options(fn () => Provider::orderBy('name')->get()->mapWithKeys(fn (Provider $provider) => [$provider->id => $provider->name])),
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListProviderPlans::route('/'),
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Filament\Resources\ProviderPlanResource\Pages;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\ProviderPlanResource;
class ListProviderPlans extends ListRecords
{
protected static string $resource = ProviderPlanResource::class;
protected function getHeaderActions(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Filament\Resources;
use Filament\Tables;
use App\Models\Provider;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Models\ProviderRegion;
use Filament\Resources\Resource;
use App\Filament\Resources\ProviderRegionResource\Pages;
class ProviderRegionResource extends Resource
{
protected static ?string $model = ProviderRegion::class;
protected static ?string $navigationIcon = 'heroicon-o-globe-americas';
protected static ?string $navigationGroup = 'Providers';
protected static ?int $navigationSort = 3;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('id')
->label('ID')
->searchable(),
Tables\Columns\TextColumn::make('provider.name')
->label(__('Provider'))
->searchable(),
Tables\Columns\TextColumn::make('region_id')
->searchable()
->label(__('Region')),
Tables\Columns\TextColumn::make('label')
->searchable()
->label(__('Label')),
])
->filters([
Tables\Filters\SelectFilter::make('provider_id')
->label(__('Provider'))
->options(fn () => Provider::orderBy('name')->pluck('name', 'id'))
])
->actions([
//
])
->bulkActions([
//
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListProviderRegions::route('/'),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ProviderRegionResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\ProviderRegionResource;
class ListProviderRegions extends ListRecords
{
protected static string $resource = ProviderRegionResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use App\Models\Provider;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Models\ProviderPlan;
use Filament\Resources\Resource;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use App\Filament\Resources\ProviderResource\Pages;
use App\Actions\Provider\SynchronizeProviderAction;
use App\Filament\Resources\ProviderResource\Widgets\AvailableProvidersOverview;
class ProviderResource extends Resource
{
protected static ?string $model = Provider::class;
protected static ?string $navigationIcon = 'heroicon-o-cloud-arrow-up';
protected static ?string $navigationGroup = 'Providers';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->label(__('Name'))
->required()
->columnSpan(2),
Forms\Components\CheckboxList::make('allowed_plans')
->options(function (Provider $record) {
return $record->plans->mapWithKeys(fn (ProviderPlan $plan) => [$plan->id => $plan->label ?? $plan->plan_id]);
})
->label(__('Allowed Plans')),
Forms\Components\CheckboxList::make('allowed_regions')
->options(fn (Provider $record) => $record->regions->pluck('label', 'id'))
->label(__('Allowed Regions')),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('id')
->label('ID')
->searchable(),
Tables\Columns\TextColumn::make('name')
->description(function (Provider $record) {
return "{$record->plans_count} plan(s) · {$record->regions_count} region(s)";
})
->label(__('Name'))
->searchable(),
Tables\Columns\TextColumn::make('label')
->label(__('Label'))
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\Action::make('synchronize_provider')
->label(__('Synchronize'))
->tooltip(__('This will synchronize the latest data from this provider to your Ploi Core installation'))
->icon('heroicon-o-arrow-path')
->action(function (Provider $record) {
$provider = app(SynchronizeProviderAction::class)->execute($record->ploi_id);
Notification::make()
->title(__('Provider :provider synchronized successfully.', ['provider' => $provider->name]))
->success()
->send();
}),
])
->bulkActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->withCount(['plans', 'regions']);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getWidgets(): array
{
return [
AvailableProvidersOverview::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListProviders::route('/'),
'synchronize' => Pages\SynchronizeProviders::route('/synchronize'),
'edit' => Pages\EditProvider::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\ProviderResource\Pages;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\ProviderResource;
class EditProvider extends EditRecord
{
protected static string $resource = ProviderResource::class;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Filament\Resources\ProviderResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\ProviderResource;
class ListProviders extends ListRecords
{
protected $listeners = [
'$refresh',
];
protected static string $resource = ProviderResource::class;
protected function getHeaderActions(): array
{
return [
Actions\Action::make('synchronize_providers')
->label(__('Synchronize providers'))
->icon('heroicon-o-arrow-path')
->color('gray')
->url(ProviderResource::getUrl('synchronize')),
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Filament\Resources\ProviderResource\Pages;
use Filament\Resources\Pages\Page;
use App\Filament\Resources\ProviderResource;
class SynchronizeProviders extends Page
{
protected static string $resource = ProviderResource::class;
protected static string $view = 'filament.resources.provider-resource.pages.synchronize-providers';
protected static ?string $title = 'Synchronize providers';
protected function getHeaderWidgets(): array
{
return [
ProviderResource\Widgets\AvailableProvidersOverview::class,
];
}
protected function getHeaderActions(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Filament\Resources\ProviderResource\Widgets;
use App\Models\AvailableProvider;
use Filament\Widgets\TableWidget;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Provider\SynchronizeProviderAction;
use Illuminate\Database\Eloquent\Relations\Relation;
class AvailableProvidersOverview extends TableWidget
{
protected $listeners = [
'$refresh',
];
protected int|string|array $columnSpan = 'full';
protected static ?string $heading = 'Available Providers';
protected function getTableDescription(): ?string
{
return 'These are all the providers available from your ploi.io account which you can synchronize to your Ploi Core installation.';
}
protected function getTableQuery(): Builder|Relation
{
return AvailableProvider::query();
}
protected function getTableColumns(): array
{
return [
TextColumn::make('name')
->label(__('Name')),
TextColumn::make('label')
->label(__('Label')),
];
}
protected function getTableActions(): array
{
return [
Action::make('synchronize_provider')
->label(__('Synchronize'))
->icon('heroicon-o-arrow-path')
->action(function (AvailableProvider $record, self $livewire) {
$provider = app(SynchronizeProviderAction::class)->execute($record->id);
$livewire->dispatch('$refresh');
Notification::make()
->title(__('Provider :provider synchronized successfully.', ['provider' => $provider->name]))
->success()
->send();
}),
];
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use Filament\Tables;
use App\Models\Redirect;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Filament\Resources\RedirectResource\Pages;
class RedirectResource extends Resource
{
protected static ?string $model = Redirect::class;
protected static ?string $navigationIcon = 'heroicon-o-arrow-top-right-on-square';
protected static ?string $navigationGroup = 'Site management';
protected static ?int $navigationSort = 5;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('site_id'),
Forms\Components\TextInput::make('server_id'),
Forms\Components\TextInput::make('status'),
Forms\Components\TextInput::make('ploi_id'),
Forms\Components\TextInput::make('redirect_from'),
Forms\Components\TextInput::make('redirect_to'),
Forms\Components\TextInput::make('type'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('redirect_from')
->wrap()
->label(__('Redirect from'))
->searchable(),
Tables\Columns\TextColumn::make('redirect_to')
->wrap()
->label(__('Redirect to'))
->searchable(),
Tables\Columns\TextColumn::make('server.name')
->label(__('Server'))
->searchable(),
Tables\Columns\TextColumn::make('site.domain')
->label(__('Site'))
->searchable(),
Tables\Columns\TextColumn::make('type')
->label(__('Type')),
Tables\Columns\TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Redirect::STATUS_BUSY => __('Busy'),
Redirect::STATUS_ACTIVE => __('Active'),
})
->colors([
'warning' => Redirect::STATUS_BUSY,
'success' => Redirect::STATUS_ACTIVE,
])
->label(__('Status')),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->actions([
//
])
->bulkActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListRedirects::route('/'),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\RedirectResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\RedirectResource;
class ListRedirects extends ListRecords
{
protected static string $resource = RedirectResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use App\Models\User;
use Filament\Tables;
use App\Models\Server;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Server\SynchronizeServerAction;
use App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource\RelationManagers;
class ServerResource extends Resource
{
protected static ?string $model = Server::class;
protected static ?string $navigationIcon = 'heroicon-o-server';
protected static ?string $navigationGroup = 'Server management';
protected static ?string $recordTitleAttribute = 'name';
public static function getGloballySearchableAttributes(): array
{
return ['name', 'ip', 'internal_ip', 'id'];
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->label(__('Name'))
->columnSpan(2),
Forms\Components\TextInput::make('ip')
->required()
->label('IP address')
->columnSpan(2),
Forms\Components\TextInput::make('internal_ip')
->required()
->label('Internal IP address')
->columnSpan(2),
Forms\Components\TextInput::make('maximum_sites')
->label(__('Maximum sites'))
->integer()
->required()
->columnSpan(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label(__('Name'))
->searchable(),
Tables\Columns\TextColumn::make('status')
->label(__('Status'))
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Server::STATUS_BUSY => __('Busy'),
Server::STATUS_ACTIVE => __('Active'),
})
->colors([
'warning' => Server::STATUS_BUSY,
'success' => Server::STATUS_ACTIVE,
]),
Tables\Columns\TextColumn::make('users')
->label(__('Users'))
->wrap()
->formatStateUsing(function (Server $record) {
$state = $record
->users
->map(function (User $user) {
return '<a href="' . UserResource::getUrl('edit', ['record' => $user]) . '" class="text-primary-600" style="white-space: nowrap">' . $user->name . '</a>';
})
->implode(', ') ?: '-';
return new HtmlString($state);
})
->searchable(query: function (Builder $query, string $search) {
return $query->whereHas('users', function (Builder $query) use ($search) {
return $query
->where('name', 'LIKE', "%{$search}%")
->orWhere('email', 'LIKE', "%{$search}%");
});
}),
Tables\Columns\TextColumn::make('maximum_sites')
->label(__('Max sites'))
->formatStateUsing(fn (Server $record) => $record->maximum_sites . " (Current: {$record->sites_count})")
->counts('sites'),
Tables\Columns\TextColumn::make('ip')
->label(__('IP')),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->dateTime(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\Action::make('synchronize_server')
->label(__('Synchronize'))
->tooltip(__('This will synchronize the latest data from this provider to your Ploi Core installation'))
->icon('heroicon-o-arrow-path')
->action(fn (Server $record) => app(SynchronizeServerAction::class)->execute($record->ploi_id))
->visible(fn (Server $record) => $record->status === Server::STATUS_ACTIVE),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->with(['users:id,name'])
->withCount(['sites']);
}
public static function getRelations(): array
{
return [
RelationManagers\UsersRelationManager::class,
RelationManagers\SitesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListServers::route('/'),
'edit' => Pages\EditServer::route('/{record}/edit'),
'synchronize' => Pages\SynchronizeServers::route('/synchronize'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use Filament\Resources\Pages\CreateRecord;
class CreateServer extends CreateRecord
{
protected static string $resource = ServerResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\ServerResource;
class EditServer extends EditRecord
{
protected static string $resource = ServerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\ServerResource;
class ListServers extends ListRecords
{
protected static string $resource = ServerResource::class;
protected function getHeaderActions(): array
{
return [
Actions\Action::make('synchronize_servers')
->label(__('Synchronize servers'))
->icon('heroicon-o-arrow-path')
->color('gray')
->url(ServerResource::getUrl('synchronize')),
...parent::getHeaderActions(),
];
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
use Filament\Actions;
use App\Models\Server;
use App\Services\Ploi\Ploi;
use Filament\Resources\Pages\Page;
use Filament\Notifications\Notification;
use App\Filament\Resources\ServerResource;
class SynchronizeServers extends Page
{
protected static string $resource = ServerResource::class;
protected static string $view = 'filament.resources.server-resource.pages.synchronize-servers';
protected static ?string $title = 'Synchronize servers';
protected function getHeaderWidgets(): array
{
return [
ServerResource\Widgets\AvailableServersOverview::class,
];
}
protected function getHeaderActions(): array
{
return [
Actions\Action::make('synchronize_servers')
->label(__('Synchronize all servers'))
->icon('heroicon-o-arrow-path')
->requiresConfirmation()
->modalHeading('Synchronize servers')
->modalDescription('This will synchronize all the servers that are listed in the table, to your Ploi Core installation.')
->action(function () {
$availableServers = Ploi::make()->synchronize()->servers()->getData();
foreach ($availableServers as $availableServer) {
Server::query()
->updateOrCreate([
'ploi_id' => $availableServer->id,
], [
'status' => $availableServer->status,
'name' => $availableServer->name,
'ip' => $availableServer->ip_address,
'ssh_port' => $availableServer->ssh_port,
'internal_ip' => $availableServer->internal_ip,
'available_php_versions' => $availableServer->installed_php_versions,
]);
}
Notification::make()
->title(__('Servers synchronized successfully.'))
->success()
->send();
}),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Filament\Resources\ServerResource\RelationManagers;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Filament\Resources\SiteResource;
use Filament\Resources\RelationManagers\RelationManager;
class SitesRelationManager extends RelationManager
{
protected static string $relationship = 'sites';
protected static ?string $recordTitleAttribute = 'domain';
public static function getLabel(): ?string
{
return __('Site');
}
protected static function getPluralModelLabel(): string
{
return __('Sites');
}
public function form(Form $form): Form
{
return SiteResource::form($form);
}
public function table(Table $table): Table
{
return SiteResource::table($table);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Filament\Resources\ServerResource\RelationManagers;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Filament\Resources\UserResource;
use Filament\Resources\RelationManagers\RelationManager;
class UsersRelationManager extends RelationManager
{
protected static string $relationship = 'users';
protected static ?string $recordTitleAttribute = 'name';
public static function getLabel(): ?string
{
return __('User');
}
protected static function getPluralModelLabel(): string
{
return __('Users');
}
public function form(Form $form): Form
{
return UserResource::form($form);
}
public function table(Table $table): Table
{
return UserResource::table($table)
->headerActions([
...$table->getHeaderActions(),
Tables\Actions\AttachAction::make()
->preloadRecordSelect(),
])
->actions([
...$table->getActions(),
Tables\Actions\DetachAction::make(),
]);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Filament\Resources\ServerResource\Widgets;
use App\Models\AvailableServer;
use Filament\Widgets\TableWidget;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Server\SynchronizeServerAction;
use Illuminate\Database\Eloquent\Relations\Relation;
class AvailableServersOverview extends TableWidget
{
protected $listeners = [
'$refresh',
];
protected int|string|array $columnSpan = 'full';
protected static ?string $heading = 'Available servers';
protected function getTableQuery(): Builder|Relation
{
return AvailableServer::query();
}
protected function getTableDescription(): ?string
{
return 'These are all the servers available from your ploi.io account which you can synchronize to your Ploi Core installation.';
}
protected function getTableColumns(): array
{
return [
TextColumn::make('name')->label(__('Name')),
TextColumn::make('ip_address')->label(__('IP address')),
TextColumn::make('sites_count')->label(__('Sites')),
];
}
protected function getTableActions(): array
{
return [
Action::make('synchronize_server')
->label(__('Synchronize'))
->icon('heroicon-o-arrow-path')
->action(function (AvailableServer $record) {
app(SynchronizeServerAction::class)->execute($record->id);
}),
];
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use App\Models\Site;
use App\Models\User;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Site\SynchronizeSiteAction;
use App\Filament\Resources\SiteResource\Pages;
use App\Filament\Resources\SiteResource\RelationManagers;
class SiteResource extends Resource
{
protected static ?string $model = Site::class;
protected static ?string $navigationIcon = 'heroicon-o-code-bracket';
protected static ?string $navigationGroup = 'Site management';
protected static ?int $navigationSort = 0;
protected static ?string $recordTitleAttribute = 'domain';
public static function getLabel(): ?string
{
return __('Site');
}
public static function getPluralLabel(): ?string
{
return __('Sites');
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('domain')
->label(__('Domain'))
->required()
->hostname()
->unique(Site::class, column: 'domain', ignoreRecord: true)
->columnSpan(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('domain')
->description(function (Site $record) {
return "PHP $record->php_version";
})
->label(__('Name'))
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('server.name')
->label(__('Server'))
->sortable()
->searchable(),
Tables\Columns\TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Site::STATUS_BUSY => __('Busy'),
Site::STATUS_ACTIVE => __('Active'),
})
->colors([
'warning' => Site::STATUS_BUSY,
'success' => Site::STATUS_ACTIVE,
])
->label(__('Status')),
Tables\Columns\TextColumn::make('users')
->label(__('Users'))
->wrap()
->formatStateUsing(function (Site $record) {
$state = $record
->users
->map(function (User $user) {
return '<a href="' . UserResource::getUrl('edit', ['record' => $user]) . '" class="text-primary-600" style="white-space: nowrap">' . $user->name . '</a>';
})
->implode(', ') ?: '-';
return new HtmlString($state);
})
->searchable(query: function (Builder $query, string $search) {
return $query->whereHas('users', function (Builder $query) use ($search) {
return $query
->where('name', 'LIKE', "%{$search}%")
->orWhere('email', 'LIKE', "%{$search}%");
});
}),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->dateTime()
->sortable(),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\Action::make('synchronize_site')
->label(__('Synchronize'))
->tooltip(__('This will synchronize the latest data from this provider to your Ploi Core installation'))
->icon('heroicon-o-arrow-path')
->action(function (Site $record) {
app(SynchronizeSiteAction::class)->execute($record->server->ploi_id, $record->ploi_id);
})
->visible(fn (Site $record) => $record->status === Site::STATUS_ACTIVE),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
])
->defaultSort('sites.created_at', 'desc');
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->with(['users', 'server']);
}
public static function getRelations(): array
{
return [
RelationManagers\UsersRelationManager::class,
RelationManagers\CertificatesRelationManager::class,
RelationManagers\CronjobsRelationManager::class,
RelationManagers\RedirectsRelationManager::class,
RelationManagers\SystemUsersRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSites::route('/'),
'edit' => Pages\EditSite::route('/{record}/edit'),
'synchronize' => Pages\SynchronizeSites::route('/synchronize'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\SiteResource\Pages;
use App\Filament\Resources\SiteResource;
use Filament\Resources\Pages\CreateRecord;
class CreateSite extends CreateRecord
{
protected static string $resource = SiteResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\SiteResource\Pages;
use Filament\Actions;
use App\Filament\Resources\SiteResource;
use Filament\Resources\Pages\EditRecord;
class EditSite extends EditRecord
{
protected static string $resource = SiteResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Filament\Resources\SiteResource\Pages;
use Filament\Actions;
use App\Traits\HasPloi;
use App\Filament\Resources\SiteResource;
use Filament\Resources\Pages\ListRecords;
class ListSites extends ListRecords
{
use HasPloi;
protected static string $resource = SiteResource::class;
protected function getHeaderActions(): array
{
return [
Actions\Action::make('synchronize_sites')
->label(__('Synchronize sites'))
->icon('heroicon-o-arrow-path')
->color('gray')
->url(SiteResource::getUrl('synchronize')),
...parent::getHeaderActions()
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Filament\Resources\SiteResource\Pages;
use App\Models\Site;
use Filament\Actions;
use App\Models\Server;
use App\Services\Ploi\Ploi;
use Filament\Resources\Pages\Page;
use App\Filament\Resources\SiteResource;
use Filament\Notifications\Notification;
class SynchronizeSites extends Page
{
protected static string $resource = SiteResource::class;
protected static string $view = 'filament.resources.site-resource.pages.synchronize-sites';
public function getHeaderWidgets(): array
{
return [
SiteResource\Widgets\AvailableSitesOverview::class,
];
}
protected function getHeaderActions(): array
{
return [
Actions\Action::make('synchronize_sites')
->label(__('Synchronize all sites'))
->icon('heroicon-o-arrow-path')
->requiresConfirmation()
->modalHeading('Synchronize sites')
->modalDescription('This will synchronize all the sites that are listed in the table, to your Ploi Core installation.')
->action(function () {
$availableSites = Ploi::make()->synchronize()->sites()->getData();
foreach ($availableSites as $availableSite) {
$server = Server::query()->where('ploi_id', $availableSite->server_id)->firstOrFail();
$site = Site::query()
->updateOrCreate([
'ploi_id' => $availableSite->id,
], [
'domain' => $availableSite->domain,
'php_version' => $availableSite->php_version,
]);
$site->status = $availableSite->status;
$site->server_id = $server->id;
$site->save();
}
Notification::make()
->title(__('Sites synchronized successfully.'))
->success()
->send();
}),
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\SiteResource\RelationManagers;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Filament\Resources\CertificateResource;
use Filament\Resources\RelationManagers\RelationManager;
class CertificatesRelationManager extends RelationManager
{
protected static string $relationship = 'certificates';
protected static ?string $recordTitleAttribute = 'domain';
public function form(Form $form): Form
{
return CertificateResource::form($form);
}
public function table(Table $table): Table
{
return CertificateResource::table($table);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\SiteResource\RelationManagers;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Filament\Resources\CronjobResource;
use Filament\Resources\RelationManagers\RelationManager;
class CronjobsRelationManager extends RelationManager
{
protected static string $relationship = 'cronjobs';
protected static ?string $recordTitleAttribute = 'command';
public function form(Form $form): Form
{
return CronjobResource::form($form);
}
public function table(Table $table): Table
{
return CronjobResource::table($table);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\SiteResource\RelationManagers;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Filament\Resources\DatabaseResource;
use Filament\Resources\RelationManagers\RelationManager;
class DatabasesRelationManager extends RelationManager
{
protected static string $relationship = 'databases';
protected static ?string $recordTitleAttribute = 'name';
public function form(Form $form): Form
{
return DatabaseResource::form($form);
}
public function table(Table $table): Table
{
return DatabaseResource::table($table);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\SiteResource\RelationManagers;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Filament\Resources\RedirectResource;
use Filament\Resources\RelationManagers\RelationManager;
class RedirectsRelationManager extends RelationManager
{
protected static string $relationship = 'redirects';
protected static ?string $recordTitleAttribute = 'from';
public function form(Form $form): Form
{
return RedirectResource::form($form);
}
public function table(Table $table): Table
{
return RedirectResource::table($table);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\SiteResource\RelationManagers;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Filament\Resources\SiteSystemUserResource;
use Filament\Resources\RelationManagers\RelationManager;
class SystemUsersRelationManager extends RelationManager
{
protected static string $relationship = 'systemUsers';
protected static ?string $recordTitleAttribute = 'user_name';
public function form(Form $form): Form
{
return SiteSystemUserResource::form($form);
}
public function table(Table $table): Table
{
return SiteSystemUserResource::table($table);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Filament\Resources\SiteResource\RelationManagers;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Filament\Resources\UserResource;
use Filament\Resources\RelationManagers\RelationManager;
class UsersRelationManager extends RelationManager
{
protected static string $relationship = 'users';
protected static ?string $recordTitleAttribute = 'name';
public static function getLabel(): ?string
{
return __('User');
}
protected static function getPluralModelLabel(): string
{
return __('Users');
}
public function form(Form $form): Form
{
return UserResource::form($form);
}
public function table(Table $table): Table
{
return UserResource::table($table)
->headerActions([
...$table->getHeaderActions(),
Tables\Actions\AttachAction::make()
->preloadRecordSelect(),
])
->actions([
...$table->getActions(),
Tables\Actions\DetachAction::make(),
]);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Filament\Resources\SiteResource\Widgets;
use App\Models\AvailableSite;
use Filament\Widgets\TableWidget;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Site\SynchronizeSiteAction;
use Illuminate\Database\Eloquent\Relations\Relation;
class AvailableSitesOverview extends TableWidget
{
protected $listeners = [
'$refresh',
];
protected int|string|array $columnSpan = 'full';
protected static ?string $heading = 'Available sites';
protected function getTableDescription(): ?string
{
return 'These are all the sites available from your ploi.io account which you can synchronize to your Ploi Core installation.';
}
protected function getTableQuery(): Builder|Relation
{
return AvailableSite::query();
}
protected function getTableColumns(): array
{
return [
TextColumn::make('domain')->label(__('Site')),
TextColumn::make('system_user')->label(__('System user')),
];
}
protected function getTableActions(): array
{
return [
Action::make('synchronize_site')
->label(__('Synchronize'))
->icon('heroicon-o-arrow-path')
->action(function (AvailableSite $record) {
app(SynchronizeSiteAction::class)->execute(ploiServerId: $record->server_id, ploiSiteId: $record->id);
}),
];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Filament\Resources;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Models\SiteSystemUser;
use Filament\Resources\Resource;
use App\Filament\Resources\SiteSystemUserResource\Pages;
class SiteSystemUserResource extends Resource
{
protected static ?string $model = SiteSystemUser::class;
protected static ?string $navigationIcon = 'heroicon-o-user-group';
protected static ?string $navigationGroup = 'Site management';
protected static ?int $navigationSort = 6;
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('user_name')
->label(__('Username'))
->searchable(),
Tables\Columns\TextColumn::make('site.domain')
->label(__('Site'))
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->label(__('Date'))
->dateTime()
->sortable(),
])
->filters([
//
])
->actions([
//
])
->bulkActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSiteSystemUsers::route('/'),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\SiteSystemUserResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\SiteSystemUserResource;
class ListSiteSystemUsers extends ListRecords
{
protected static string $resource = SiteSystemUserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Filament\Resources;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use Laravel\Cashier\Subscription;
use App\Filament\Resources\SubscriptionResource\Pages;
class SubscriptionResource extends Resource
{
protected static ?string $model = Subscription::class;
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
protected static ?int $navigationSort = 4;
public static function getSlug(): string
{
return 'subscriptions';
}
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('user.name')
->searchable()
->url(fn ($record) => UserResource::getUrl('edit', ['record' => $record])),
Tables\Columns\TextColumn::make('stripe_id')->searchable(),
Tables\Columns\TextColumn::make('stripe_plan')->searchable(),
Tables\Columns\TextColumn::make('stripe_status')
->label('Status')
->badge()
->colors([
'success' => \Stripe\Subscription::STATUS_ACTIVE,
'warning' => \Stripe\Subscription::STATUS_PAST_DUE,
]),
Tables\Columns\TextColumn::make('created_at')
->sortable()
->dateTime()
->label(__('Date'))
])
->filters([
//
])
->actions([
// Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSubscriptions::route('/'),
// 'create' => Pages\CreateSubscription::route('/create'),
// 'edit' => Pages\EditSubscription::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\SubscriptionResource\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\SubscriptionResource;
class CreateSubscription extends CreateRecord
{
protected static string $resource = SubscriptionResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\SubscriptionResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\SubscriptionResource;
class EditSubscription extends EditRecord
{
protected static string $resource = SubscriptionResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\SubscriptionResource\Pages;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\SubscriptionResource;
class ListSubscriptions extends ListRecords
{
protected static string $resource = SubscriptionResource::class;
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\Filament\Resources;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use App\Models\SupportTicket;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
use App\Filament\Resources\SupportTicketResource\Pages;
class SupportTicketResource extends Resource
{
protected static ?string $model = SupportTicket::class;
protected static ?string $navigationGroup = 'Support';
protected static ?string $navigationIcon = 'heroicon-o-lifebuoy';
protected static ?string $label = 'Ticket';
protected static ?string $pluralLabel = 'Tickets';
public static function shouldRegisterNavigation(): bool
{
return (bool)setting('support');
}
public static function getNavigationBadge(): ?string
{
return static::getEloquentQuery()->count();
}
public static function form(Form $form): Form
{
return $form
->schema([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('status')
->label(__('Status'))
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
SupportTicket::STATUS_OPEN => __('Open'),
SupportTicket::STATUS_CLOSED => __('Closed'),
SupportTicket::STATUS_CUSTOMER_REPLY => __('Customer Reply'),
SupportTicket::STATUS_SUPPORT_REPLY => __('Support Reply'),
})
->colors([
'primary' => [SupportTicket::STATUS_OPEN, SupportTicket::STATUS_SUPPORT_REPLY, SupportTicket::STATUS_CUSTOMER_REPLY],
'danger' => SupportTicket::STATUS_CLOSED,
])
->wrap(false),
Tables\Columns\TextColumn::make('title')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('replies_count')
->label(__('Replies'))
->getStateUsing(fn (SupportTicket $record) => $record->replies->count()),
Tables\Columns\TextColumn::make('user.name')
->searchable()
->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('status')
->label(__('Status'))
->multiple()
->options([
SupportTicket::STATUS_OPEN => __('Open'),
SupportTicket::STATUS_CLOSED => __('Closed'),
SupportTicket::STATUS_CUSTOMER_REPLY => __('Customer Reply'),
SupportTicket::STATUS_SUPPORT_REPLY => __('Support Reply'),
])
->default([
SupportTicket::STATUS_CUSTOMER_REPLY,
SupportTicket::STATUS_OPEN,
]),
]);
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->with(['user', 'replies'])
->orderByDesc('updated_at');
}
public static function getPages(): array
{
return [
'index' => Pages\ListSupportTickets::route('/'),
'view' => Pages\ViewSupportTicket::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\SupportTicketResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\SupportTicketResource;
class ListSupportTickets extends ListRecords
{
protected static string $resource = SupportTicketResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Filament\Resources\SupportTicketResource\Pages;
use Filament\Actions;
use App\Models\SupportTicket;
use Filament\Resources\Pages\Page;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Filament\Notifications\Notification;
use App\Mail\Support\TicketRepliedToEmail;
use Filament\Forms\Components\MarkdownEditor;
use App\Filament\Resources\SupportTicketResource;
class ViewSupportTicket extends Page
{
protected $listeners = [
'$refresh',
];
protected static string $resource = SupportTicketResource::class;
protected static string $view = 'filament.resources.support-ticket-resource.pages.view-support-ticket';
public SupportTicket $record;
public array $data = [];
public function getTitle(): string
{
return __('View ticket') . ': ' . $this->record->title;
}
protected function getHeaderActions(): array
{
return [
Actions\Action::make('close')
->label(__('Close'))
->action(function (self $livewire) {
$livewire->record->status = SupportTicket::STATUS_CLOSED;
$livewire->record->save();
Notification::make()
->title(__('Ticket closed'))
->success()
->send();
$livewire->redirect(SupportTicketResource::getUrl('view', ['record' => $livewire->getRecord()]));
})
->visible(fn (self $livewire) => $livewire->record->status !== SupportTicket::STATUS_CLOSED)
->color('danger'),
Actions\Action::make('reopen')
->label(__('Reopen'))
->action(function (self $livewire) {
$livewire->record->status = SupportTicket::STATUS_OPEN;
$livewire->record->save();
Notification::make()
->title(__('Ticket reopened'))
->success()
->send();
$livewire->redirect(SupportTicketResource::getUrl('view', ['record' => $livewire->getRecord()]));
})
->visible(fn (self $livewire) => $livewire->record->status === SupportTicket::STATUS_CLOSED)
->color('primary'),
];
}
public function mount(): void
{
$this->form->fill();
}
protected function getFormSchema(): array
{
return [
MarkdownEditor::make('content')
->label(__('Reply'))
->required(),
];
}
protected function getFormStatePath(): ?string
{
return 'data';
}
public function reply(): void
{
$state = $this->form->getStateOnly(['content']);
$this->record->status = SupportTicket::STATUS_SUPPORT_REPLY;
$this->record->save();
$reply = $this->record->replies()->create($state);
$reply->user_id = Auth::id();
$reply->save();
Mail::to($this->record->user)->send(new TicketRepliedToEmail($this->record));
$this->form->fill();
$this->dispatch('$refresh');
Notification::make()
->title(__('Reply sent'))
->success()
->send();
}
public function close(): void
{
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Filament\Resources;
use Filament\Forms;
use App\Models\User;
use Filament\Tables;
use Filament\Forms\Form;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use STS\FilamentImpersonate\Impersonate;
use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'heroicon-o-users';
protected static ?int $navigationSort = 2;
protected static ?string $recordTitleAttribute = 'name';
public static function getGloballySearchableAttributes(): array
{
return ['user_name', 'name', 'email'];
}
public static function getLabel(): ?string
{
return __('User');
}
public static function getPluralLabel(): ?string
{
return __('Users');
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->label(__('Name'))
->required(),
Forms\Components\TextInput::make('email')
->label(__('E-mail address'))
->email()
->unique(table: User::class, column: 'email', ignoreRecord: true)
->required(),
Forms\Components\Select::make('role')
->options([
User::ADMIN => __('Administrator'),
User::USER => __('User'),
])
->default(User::USER)
->columnSpan(2)
->required(),
Forms\Components\Select::make('package_id')
->label(__('Package'))
->relationship('package', 'name'),
Forms\Components\Select::make('language')
->label(__('Language'))
->default('en')
->options(collect(languages())->mapWithKeys(fn (string $language) => [$language => $language])),
Forms\Components\Textarea::make('notes')
->label(__('Notes'))
->maxLength(65535),
Forms\Components\Textarea::make('blocked')
->helperText('Entering a reason here will block the user from accessing your panel. It will also display the typed message to the user.')
->label(__('Blocked')),
Forms\Components\Checkbox::make('requires_password_for_ftp')
->default(true)
->label(__('Require password to show FTP password'))
->helperText(__('Disabling this will allow this user to get the FTP password without entering their password.')),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->label(__('Name'))
->searchable(),
Tables\Columns\TextColumn::make('user_name')
->label(__('User name'))
->searchable(),
Tables\Columns\TextColumn::make('email')
->label(__('E-mail address'))
->searchable(),
Tables\Columns\TextColumn::make('servers_count')
->label(__('Servers'))
->counts('servers')
->sortable(),
Tables\Columns\TextColumn::make('sites_count')
->label(__('Sites'))
->counts('sites')
->sortable(),
Tables\Columns\TextColumn::make('role')
->label(__('Role')),
Tables\Columns\TextColumn::make('package.name')
->label(__('Package')),
])
->filters([
//
])
->actions([
Impersonate::make('impersonate')
->tooltip('Login as this user (impersonate)')
->visible(fn () => config('core.impersonation')),
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
])
->defaultSort('users.created_at', 'desc');
}
public static function getRelations(): array
{
return [
RelationManagers\SitesRelationManager::class,
RelationManagers\ServersRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
}

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