Compare commits

...

51 Commits

Author SHA1 Message Date
Charles
feadaa2caf Add Kill button to console (#791)
* Add Kill button to console

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

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

---------

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

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

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

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

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

---------

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

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

* Update Console Page

* Edit console input

* oops

* Remove failed attempt of clearing input when server offline

* Update File Editor to match console

* a touch more style

* Show not-allowed on read-only input

* round bottom corners of command input

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

* Remove this also

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

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

* Update laravel and migrations

* WIP

* fix tests

* Update composer

* Fix transformer

* Fix filament pages

* WIP

* Update DatabaseHostTransformer

* fix: tests

* pint this files pls

* resolve merge better

* Update migration

* Update Migration, Again

* Update down migration

---------

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

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

Updates console to be more better <3.

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

* Add overflow to... Everything?

* Oops, Add Name label back

* Actually handle Transfer Status & remove useless switch

* Use switch case

* Readonly command input if server can't receive one

* lint

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

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

* Use filament::icon instead of raw svg

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

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

---------

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

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

---------

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

* add egg filter

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

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

* fix "getToken"

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

---------

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

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

* add button to open server in admin area

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

* hide schedule presets on view

* use arrow functions for auth checks

* add placeholder to schedules last run

* change icon of "open in admin"

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

* throw Halt instead of return

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

* Remove unique, make email lowercase in request

* Remove 'kill' option, not used.

* Prevent users from editing them selves

---------

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

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

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

* Add some basic resource pages

* Wip

* Wip terminal

* Wip

* Add new panel

* Add some basic resource pages

* Wip

* [Sub-Users] Add Invite

TODO: The logic with permissions

* [Sub-Users] Fix Creation

* [Cron] Add basics

* Add basic auth and messages

* Add basic buttons

* WIP on issue/353

* WIP on issue/353

* Add Database page

* Update Database Page

* Start of Backup Page

* Composer Update

* Changes

* Send input

* Remove this includes

* Better offline handling

* Consolidate top nav config

* Update Backups Page

* Update Backups

* Change name

* Add Assign All, Layout Fixes.

* conflict

* update schedule pages

* fix phpstan

* update pint.json

* add cron presets to schedule

* fix tests

* fix task creation

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

* update activity pages

* update resources

* Update Edit User

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

* .... Sure it works.

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

* user: update canAccessPanel & canAccessTenant

* add helper to convert bytes into readable format

* very basic file explorer

* files: fix some stuff & remove dummy data

* files: better error handling

* files: basic file editor

* files: add some actions

* File manager updates

* files: fix paths

* Revery Composer Upgrade, Fixes SQLite

* fix: Pint (#517)

feat: MenuItems to and from admin

* Update File Editing

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

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

* add enum for editor langs

* files: add upload & pull actions

* fix build

* files: handle images

* Update to Filament v3.2.98

* files: add remaining actions

* use `authorize` instead of `hidden`

* fix canAccessTenant

* update date columns

* files: testing & fixes

* Fix File Names

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

* Combine Pull/Upload

* Fix BulkDelete

* Uncontained tabs

* Hide Lang Selection, Move Actions

* Update Monaco, more custom

* Add livewire config

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

* files: fix record url

* basic setup for settings & startup page

* make abstract class for simple app pages

* Basic Startup Page

* Update nav sort

* small cleanup

* startup: fix shouldHideComponent & getSelectOptionsFromRules

* startup: fix non editable fields & set default value

* startup: add todo for save button

* Save Variables after update & off click

Variables update when the user clicks off the input.

* Notifications are cool

* Add rule validation

* Sort variables by sortid

* pint

* Settings Page + Startup Changes

* settings: cleanup

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

* Use Repeater for variables

* Add Network, Remove breadcrumbs

* Add paginated to file explorer

* Fix updating variables

* Add link to go to new client area

* fix after merge

* Add graphs to console page

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

* fix pint & phpstan

* fix authorizeAccess for EditFiles and Startup page

* Fix rules on startup page

* Update console size

* Fix node name

* add "global search" to files list

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

* remove debug dummy data

* update view action on ListServers

* enable SPA mode for app panel

* remove colors from app panel

they are defined globally in AppServiceProvider

* update global search ui a bit

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

* add own page for global search

untested - and route needs cleanup (if possible)

* fix File getRows

* remove "path" from SearchFiles (for now)

* fix caching for searched files

* add title and breadcrumbs to global search page

* make cpu & memory charts on console page working

* fix phpstan

* add missing import

* cleanup console views & widgets

* add overview stats to console

* don't be so lazy, console!

* make history working

* decode data to get array

* add missing On

* fix json_decode

* change polling to 1 sec

* hide "0" cpu/ memory

* add data to network chart

* Remove data labels

* fix data on network chart

* fix data on network chart (2nd try)

* WIP Network Stats

* Remove test

* Change MaxWidth

* run pint

* fix phpstan

* Fix storeStats cast

* make $data a string

this time for real

* update visible check for "admin" menu item

* remove account widget

* rebrand "Dashboard" to "Server List"

WIP - doesn't look good but is somewhat working

* fix canAccessPanel

* separate server list into own panel

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

* display correct icon and color on server list entries

* show total memory if server is offline

* replace custom server list page with ListRecords page

* fix tests

* fix namespace

* remove "open" button and make whole column clickable

* Update EditProfile

* run pint

* fix access to server list

* add new login page to panels

* fix next_run_at for new schedules

* use new DateTimeColumn

* add own column for file bytes

* return to server list when clicking title

* fix console loading

* handle server with "conflict state"

* add banner if server is in "conflict state"

* fix phpstan

* update docker image select

* fix permission checks on Settings & Startup pages

* fix query for activity log page

* fix activity log not being logged

* adjust ListActivities

* fix phpstan

* fix pint

* fix profile menu item link on server panel

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

* change backup icon

* update navigation sort

* general code cleanup

* more cleanup

* Disable Restart/Stop if server is offline

* Change rename notification

* Remove negation on abort_unless

* Add notification on save

* Single disabled closure & comment unused import

* Add required to Server Name & Nullable to description

* mutateFormDataBeforeSave doesn't work since we use forceFill

* Fix web socket connection not existing.

* Fix some subuser permissions

* add permission checks to resources

* do not allow self-deletion

* Update editing file permissions

* Fix of the previous fix

* add service for subuser updating

* Only allow save if they have file_update

* Remove unused import

* Update backup delete button

* Add Delete, remove bulks

* Update Database page

* Use Allocation Permissions

* add canAccess check to startup

* Add Permission checks to Settings page

* add service for subuser deletion

* Remove Kill permission

* Updates

* fix move files

* add redirects

* fix phpstan

* activity: remove properties from tans for now

* If alias, use that, else ip

---------

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

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

* Remove unused function

* readd function

* replace refreshform function

* add authorize, remove database limit check

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

* add validation, match old client area more

* Add more authorize to Database tab

* Add confirmation to delete

* make password hidden / revealable

* better clarification

* Set default and remove placeholder.

* Remove server import, add database model to auth

* Make same changes for the database host page

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

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

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

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

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

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

* Remove each hidden

* Return nothing if user has no perms

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

* Fix view permission for relationship manager

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

* Pint

---------

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

* add auth frontend

* add configs for all default providers

* add more default providers

* add env variables to enable oauth providers

* small refactor to link/ unlink routes

* add oauth tab to (admin) profile

* use redirects instead of exceptions

* add notification if no oauth user is found

* use import in config

* remove whmcs provider

* replace hardcoded links with `route`

* redirect to account page on unlink

* remove unnecessary controller and handle linking/ unlinking in action

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

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

* run pint

* fix phpstan

* update migrations (sqlite `dropForeign`)

* fix migrations

* Reset these back for now

* Alphabetize the rules

* run `php artisan filament:upgrade`

---------

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

* remove asDateTime overwrite

* add custom column to display dates in user timezone

* use `APP_TIMEZONE` as default timezone for new users

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

* make login case insensitive

* fix tests
2024-11-15 20:39:06 +01:00
MartinOscar
24eb52f7d6 Merge pull request #709 from pelican-dev/charles/fixversio
Fix Panel Version Cache
2024-11-14 01:12:09 +01:00
notCharles
d87d3760a1 Fix Panel Version Cache 2024-11-13 19:08:46 -05:00
282 changed files with 7514 additions and 2115 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
<?php
namespace App\Filament\Pages;
namespace App\Filament\Admin\Pages;
use App\Filament\Resources\NodeResource\Pages\CreateNode;
use App\Filament\Resources\NodeResource\Pages\ListNodes;
use App\Filament\Admin\Resources\NodeResource\Pages\CreateNode;
use App\Filament\Admin\Resources\NodeResource\Pages\ListNodes;
use App\Models\Egg;
use App\Models\Node;
use App\Models\Server;

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Pages;
namespace App\Filament\Admin\Pages;
use App\Models\Backup;
use App\Notifications\MailTested;
@@ -10,6 +10,7 @@ use Filament\Actions\Action;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
@@ -22,9 +23,9 @@ use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page;
use Filament\Support\Enums\MaxWidth;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Artisan;
@@ -37,7 +38,6 @@ use Illuminate\Support\HtmlString;
class Settings extends Page implements HasForms
{
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;
use InteractsWithHeaderActions;
@@ -185,6 +185,11 @@ class Settings extends Page implements HasForms
$set('TRUSTED_PROXIES', $ips->values()->all());
}),
]),
Select::make('FILAMENT_WIDTH')
->label('Display Width')
->native(false)
->options(MaxWidth::class)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
];
}
@@ -583,11 +588,6 @@ class Settings extends Page implements HasForms
return 'data';
}
protected function hasUnsavedDataChangesAlert(): bool
{
return true;
}
public function save(): void
{
try {
@@ -601,8 +601,6 @@ class Settings extends Page implements HasForms
Artisan::call('config:clear');
Artisan::call('queue:restart');
$this->rememberData();
$this->redirect($this->getUrl());
Notification::make()

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Filament\Resources\ApiKeyResource\Pages;
use App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Models\ApiKey;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\ApiKeyResource\Pages;
namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Resources\ApiKeyResource;
use App\Filament\Admin\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
<?php
namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
namespace App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers;
use App\Models\Database;
use App\Services\Databases\DatabasePasswordService;
use App\Tables\Columns\DateTimeColumn;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
@@ -23,21 +24,30 @@ class DatabasesRelationManager extends RelationManager
{
return $form
->schema([
TextInput::make('database')->columnSpanFull(),
TextInput::make('database')
->columnSpanFull(),
TextInput::make('username'),
TextInput::make('password')
->password()
->revealable()
->hintAction(
Action::make('rotate')
->icon('tabler-refresh')
->requiresConfirmation()
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
->authorize(fn (Database $database) => auth()->user()->can('update database', $database))
)
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')->label('Connections From'),
TextInput::make('max_connections'),
TextInput::make('remote')
->label('Connections From')
->formatStateUsing(fn ($record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote),
TextInput::make('max_connections')
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections),
TextInput::make('JDBC')
->label('JDBC Connection String')
->columnSpanFull()
->password()
->revealable()
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
]);
}
@@ -47,18 +57,25 @@ class DatabasesRelationManager extends RelationManager
return $table
->recordTitleAttribute('servers')
->columns([
TextColumn::make('database')->icon('tabler-database'),
TextColumn::make('username')->icon('tabler-user'),
TextColumn::make('remote'),
TextColumn::make('database')
->icon('tabler-database'),
TextColumn::make('username')
->icon('tabler-user'),
TextColumn::make('remote')
->formatStateUsing(fn ($record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote),
TextColumn::make('server.name')
->icon('tabler-brand-docker')
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
TextColumn::make('max_connections'),
TextColumn::make('created_at')->dateTime(),
TextColumn::make('max_connections')
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections),
DateTimeColumn::make('created_at'),
])
->actions([
DeleteAction::make(),
ViewAction::make()->color('primary'),
DeleteAction::make()
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
ViewAction::make()
->color('primary')
->hidden(fn () => !auth()->user()->can('viewList database')),
]);
}

View File

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

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\DatabaseResource\Pages;
namespace App\Filament\Admin\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use App\Filament\Admin\Resources\DatabaseResource;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\DatabaseResource\Pages;
namespace App\Filament\Admin\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use App\Filament\Admin\Resources\DatabaseResource;
use Filament\Actions;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;

View File

@@ -1,8 +1,9 @@
<?php
namespace App\Filament\Resources\DatabaseResource\Pages;
namespace App\Filament\Admin\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use App\Filament\Admin\Resources\DatabaseResource;
use App\Tables\Columns\DateTimeColumn;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
@@ -34,12 +35,10 @@ class ListDatabases extends ListRecords
TextColumn::make('max_connections')
->numeric()
->sortable(),
TextColumn::make('created_at')
->dateTime()
DateTimeColumn::make('created_at')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
DateTimeColumn::make('updated_at')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])

View File

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

View File

@@ -1,9 +1,9 @@
<?php
namespace App\Filament\Resources\EggResource\Pages;
namespace App\Filament\Admin\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Resources\EggResource;
use App\Filament\Admin\Resources\EggResource;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;

View File

@@ -1,10 +1,10 @@
<?php
namespace App\Filament\Resources\EggResource\Pages;
namespace App\Filament\Admin\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Resources\EggResource;
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
use App\Filament\Admin\Resources\EggResource;
use App\Filament\Admin\Resources\EggResource\RelationManagers\ServersRelationManager;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;
@@ -229,6 +229,7 @@ class EditEgg extends EditRecord
->default('ash'),
MonacoEditor::make('script_install')
->label('Install Script')
->placeholderText('')
->columnSpanFull()
->fontSize('16px')
->language('shell')

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\EggResource\Pages;
namespace App\Filament\Admin\Resources\EggResource\Pages;
use App\Filament\Resources\EggResource;
use App\Filament\Admin\Resources\EggResource;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\NodeResource\Pages;
namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Filament\Admin\Resources\NodeResource;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Resources\NodeResource\RelationManagers;
namespace App\Filament\Admin\Resources\NodeResource\RelationManagers;
use App\Models\Allocation;
use App\Models\Node;
@@ -47,16 +47,20 @@ class AllocationsRelationManager extends RelationManager
// All assigned allocations
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
->paginationPageOptions(['10', '20', '50', '100', '200', '500', '1000'])
->searchable()
->selectCurrentPageOnly() //Prevent people from trying to nuke 30,000 ports at once.... -,-
->columns([
TextColumn::make('id'),
TextColumn::make('id')
->toggleable()
->toggledHiddenByDefault(),
TextColumn::make('port')
->searchable()
->label('Port'),
TextColumn::make('server.name')
->label('Server')
->icon('tabler-brand-docker')
->visibleFrom('md')
->searchable()
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
TextInputColumn::make('ip_alias')

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Enums\RolePermissionModels;
use App\Enums\RolePermissionPrefixes;
use App\Filament\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource\Pages;
use App\Models\Role;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
@@ -91,6 +91,8 @@ class RoleResource extends Resource
$icon = ('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon();
} elseif (class_exists('\App\Filament\Pages\\' . $model)) {
$icon = ('\App\Filament\Pages\\' . $model)::getNavigationIcon();
} elseif (class_exists('\App\Filament\Server\Resources\\' . $model . 'Resource')) {
$icon = ('\App\Filament\Server\Resources\\' . $model . 'Resource')::getNavigationIcon();
}
return Section::make(Str::headline(Str::plural($model)))

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\RoleResource\Pages;
namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use App\Filament\Admin\Resources\RoleResource;
use App\Models\Role;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Arr;

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\Node;
@@ -113,6 +113,7 @@ class CreateServer extends CreateRecord
TextInput::make('username')
->alphaNum()
->required()
->minLength(3)
->maxLength(255),
TextInput::make('email')

View File

@@ -1,12 +1,14 @@
<?php
namespace App\Filament\Resources\ServerResource\Pages;
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Enums\ContainerStatus;
use App\Enums\ServerState;
use App\Filament\Resources\ServerResource;
use App\Filament\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
use App\Filament\Server\Pages\Console;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Egg;
use App\Models\Mount;
use App\Models\Server;
@@ -511,23 +513,19 @@ class EditServer extends EditRecord
->label('Startup Command')
->required()
->columnSpan(6)
->rows(function ($state) {
return str($state)->explode("\n")->reduce(
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
0
);
}),
->autosize(),
Textarea::make('defaultStartup')
->hintAction(CopyAction::make())
->label('Default Startup Command')
->disabled()
->autosize()
->columnSpan(6)
->formatStateUsing(function ($state, Get $get) {
$egg = Egg::query()->find($get('egg_id'));
return $egg->startup;
})
->columnSpan(6),
}),
Repeater::make('server_variables')
->relationship('serverVariables', function (Builder $query) {
@@ -608,7 +606,9 @@ class EditServer extends EditRecord
->columnSpanFull(),
]),
Tab::make('Databases')
->hidden(fn () => !auth()->user()->can('viewList database'))
->icon('tabler-database')
->columns(4)
->schema([
Repeater::make('databases')
->grid()
@@ -622,35 +622,50 @@ class EditServer extends EditRecord
->formatStateUsing(fn ($record) => $record->database)
->hintAction(
Action::make('Delete')
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database))
->color('danger')
->icon('tabler-trash')
->action(fn (DatabaseManagementService $databaseManagementService, $record) => $databaseManagementService->delete($record))
->requiresConfirmation()
->modalIcon('tabler-database-x')
->modalHeading('Delete Database?')
->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
->modalDescription(fn (Get $get) => 'Are you sure you want to delete ' . $get('database') . '?')
->action(function (DatabaseManagementService $databaseManagementService, $record) {
$databaseManagementService->delete($record);
$this->fillForm();
})
),
TextInput::make('username')
->disabled()
->formatStateUsing(fn ($record) => $record->username)
->columnSpan(2),
->columnSpan(1),
TextInput::make('password')
->disabled()
->password()
->revealable()
->columnSpan(1)
->hintAction(
Action::make('rotate')
->authorize(fn (Database $database) => auth()->user()->can('update database', $database))
->icon('tabler-refresh')
->requiresConfirmation()
->modalHeading('Change Database Password?')
->action(fn (DatabasePasswordService $service, $record, $set, $get) => $this->rotatePassword($service, $record, $set, $get))
->requiresConfirmation()
)
->formatStateUsing(fn (Database $database) => $database->password)
->columnSpan(2),
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->disabled()
->formatStateUsing(fn ($record) => $record->remote)
->formatStateUsing(fn ($record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote)
->columnSpan(1)
->label('Connections From'),
TextInput::make('max_connections')
->disabled()
->formatStateUsing(fn ($record) => $record->max_connections)
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections)
->columnSpan(1),
TextInput::make('JDBC')
->disabled()
->password()
->revealable()
->label('JDBC Connection String')
->columnSpan(2)
->formatStateUsing(fn (Get $get, $record) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($record->password) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database')),
@@ -659,7 +674,58 @@ class EditServer extends EditRecord
->deletable(false)
->addable(false)
->columnSpan(4),
])->columns(4),
Forms\Components\Actions::make([
Action::make('createDatabase')
->authorize(fn () => auth()->user()->can('create database'))
->disabled(fn () => DatabaseHost::query()->count() < 1)
->label(fn () => DatabaseHost::query()->count() < 1 ? 'No Database Hosts' : 'Create Database')
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
->modalSubmitActionLabel('Create Database')
->action(function (array $data, DatabaseManagementService $service, Server $server, RandomWordService $randomWordService) {
if (empty($data['database'])) {
$data['database'] = $randomWordService->word() . random_int(1, 420);
}
if (empty($data['remote'])) {
$data['remote'] = '%';
}
$data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id);
try {
$service->setValidateDatabaseLimit(false)->create($server, $data);
} catch (Exception $e) {
Notification::make()
->title('Failed to Create Database')
->body($e->getMessage())
->danger()
->persistent()->send();
}
$this->fillForm();
})
->form([
Select::make('database_host_id')
->label('Database Host')
->required()
->placeholder('Select Database Host')
->relationship('node.databaseHosts', 'name',
fn (Builder $query, Server $server) => $query->whereRelation('nodes', 'nodes.id', $server->node_id))
->default(fn () => (DatabaseHost::query()->first())?->id)
->selectablePlaceholder(false),
TextInput::make('database')
->label('Database Name')
->alphaDash()
->prefix(fn (Server $server) => 's' . $server->id . '_')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Leaving this blank will auto generate a random name'),
TextInput::make('remote')
->columnSpan(1)
->regex('/^[\w\-\/.%:]+$/')
->label('Connections From')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Where connections should be allowed from. Leave blank to allow connections from anywhere.'),
]),
])->alignCenter()->columnSpanFull(),
]),
Tab::make('Actions')
->icon('tabler-settings')
->schema([
@@ -800,13 +866,13 @@ class EditServer extends EditRecord
->action(function (Server $server, ServerDeletionService $service) {
$service->handle($server);
return redirect(ListServers::getUrl());
return redirect(ListServers::getUrl(panel: 'admin'));
})
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
Actions\Action::make('console')
->label('Console')
->icon('tabler-terminal')
->url(fn (Server $server) => "/server/$server->uuid_short"),
->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)),
$this->getSaveFormAction()->formId('form'),
];

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
<?php
namespace App\Filament\Resources;
namespace App\Filament\Admin\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource\RelationManagers\ServersRelationManager;
use App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource\RelationManagers;
use App\Models\User;
use Filament\Resources\Resource;
@@ -23,7 +23,7 @@ class UserResource extends Resource
public static function getRelations(): array
{
return [
ServersRelationManager::class,
RelationManagers\ServersRelationManager::class,
];
}

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use App\Filament\Admin\Resources\UserResource;
use App\Models\Role;
use App\Models\User;
use Filament\Actions\DeleteAction;
@@ -24,7 +24,7 @@ class EditUser extends EditRecord
return $form
->schema([
Section::make()->schema([
TextInput::make('username')->required()->maxLength(255),
TextInput::make('username')->required()->minLength(3)->maxLength(255),
TextInput::make('email')->email()->required()->maxLength(255),
TextInput::make('password')
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use App\Filament\Admin\Resources\UserResource;
use App\Models\Role;
use App\Models\User;
use App\Services\Users\UserCreationService;
@@ -50,12 +50,13 @@ class ListUsers extends ListRecords
->label('2FA')
->visibleFrom('lg')
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
->boolean()->sortable(),
TextColumn::make('roles_count')
->counts('roles')
->icon('tabler-users-group')
->boolean()
->sortable(),
TextColumn::make('roles.name')
->label('Roles')
->formatStateUsing(fn (User $user, $state) => $state . ($user->isRootAdmin() ? ' (Root Admin)' : '')),
->badge()
->icon('tabler-users-group')
->placeholder('No roles'),
TextColumn::make('servers_count')
->counts('servers')
->icon('tabler-server')
@@ -65,7 +66,6 @@ class ListUsers extends ListRecords
->label('Subusers')
->counts('subusers')
->icon('tabler-users'),
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
])
->actions([
EditAction::make(),
@@ -92,6 +92,7 @@ class ListUsers extends ListRecords
->alphaNum()
->required()
->unique()
->minLength(3)
->maxLength(255),
TextInput::make('email')
->email()

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Resources\WebhookResource\Pages;
namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Resources\WebhookResource;
use App\Filament\Admin\Resources\WebhookResource;
use App\Models\WebhookConfiguration;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;

View File

@@ -1,9 +1,9 @@
<?php
namespace App\Filament\Resources\WebhookResource\Pages;
namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Models\WebhookConfiguration;
use App\Filament\Resources\WebhookResource;
use App\Filament\Admin\Resources\WebhookResource;
use Filament\Actions;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;

View File

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

View File

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

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Filament\App\Resources\ServerResource\Pages;
use App\Filament\App\Resources\ServerResource;
use App\Filament\Server\Pages\Console;
use App\Models\Server;
use App\Tables\Columns\ServerEntryColumn;
use Carbon\CarbonInterface;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\Layout\Stack;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Illuminate\Support\Number;
class ListServers extends ListRecords
{
protected static string $resource = ServerResource::class;
public function table(Table $table): Table
{
$baseQuery = auth()->user()->can('viewList server') ? Server::query() : auth()->user()->accessibleServers();
return $table
->paginated(false)
->query(fn () => $baseQuery)
->poll('15s')
->columns([
Stack::make([
ServerEntryColumn::make('server_entry')
->searchable(['name']),
]),
])
->contentGrid([
'default' => 1,
'xl' => 2,
])
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
->emptyStateIcon('tabler-brand-docker')
->emptyStateDescription('')
->emptyStateHeading('You don\'t have access to any servers!')
->filters([
TernaryFilter::make('only_my_servers')
->label('Owned by')
->placeholder('All servers')
->trueLabel('My Servers')
->falseLabel('Others\' Servers')
->default()
->queries(
true: fn (Builder $query) => $query->where('owner_id', auth()->user()->id),
false: fn (Builder $query) => $query->whereNot('owner_id', auth()->user()->id),
blank: fn (Builder $query) => $query,
),
SelectFilter::make('egg')
->relationship('egg', 'name', fn (Builder $query) => $query->whereIn('id', $baseQuery->pluck('egg_id')))
->searchable()
->preload(),
]);
}
// @phpstan-ignore-next-line
private function uptime(Server $server): string
{
$uptime = Arr::get($server->resources(), 'uptime', 0);
if ($uptime === 0) {
return 'Offline';
}
return now()->subMillis($uptime)->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE, short: true, parts: 2);
}
// @phpstan-ignore-next-line
private function cpu(Server $server): string
{
$cpu = Number::format(Arr::get($server->resources(), 'cpu_absolute', 0), maxPrecision: 2, locale: auth()->user()->language) . '%';
$max = Number::format($server->cpu, locale: auth()->user()->language) . '%';
return $cpu . ($server->cpu > 0 ? ' Of ' . $max : '');
}
// @phpstan-ignore-next-line
private function memory(Server $server): string
{
$latestMemoryUsed = Arr::get($server->resources(), 'memory_bytes', 0);
$totalMemory = Arr::get($server->resources(), 'memory_limit_bytes', 0);
$used = config('panel.use_binary_prefix')
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
if ($totalMemory === 0) {
$total = config('panel.use_binary_prefix')
? Number::format($server->memory / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($server->memory / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
} else {
$total = config('panel.use_binary_prefix')
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
}
return $used . ($server->memory > 0 ? ' Of ' . $total : '');
}
// @phpstan-ignore-next-line
private function disk(Server $server): string
{
$usedDisk = Arr::get($server->resources(), 'disk_bytes', 0);
$used = config('panel.use_binary_prefix')
? Number::format($usedDisk / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($usedDisk / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
$total = config('panel.use_binary_prefix')
? Number::format($server->disk / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($server->disk / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
return $used . ($server->disk > 0 ? ' Of ' . $total : '');
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
namespace App\Filament\Pages\Auth;
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
use App\Facades\Activity;
@@ -9,13 +9,13 @@ use App\Models\ApiKey;
use App\Models\User;
use App\Services\Users\ToggleTwoFactorService;
use App\Services\Users\TwoFactorSetupService;
use App\Services\Users\UserUpdateService;
use chillerlan\QRCode\Common\EccLevel;
use chillerlan\QRCode\Common\Version;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Closure;
use DateTimeZone;
use Exception;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
@@ -29,16 +29,21 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\Auth\EditProfile as BaseEditProfile;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Laravel\Socialite\Facades\Socialite;
/**
* @method User getUser()
*/
class EditProfile extends \Filament\Pages\Auth\EditProfile
class EditProfile extends BaseEditProfile
{
private ToggleTwoFactorService $toggleTwoFactorService;
@@ -47,6 +52,11 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
$this->toggleTwoFactorService = $toggleTwoFactorService;
}
public function getMaxWidth(): MaxWidth|string
{
return config('panel.filament.display-width', 'screen-2xl');
}
protected function getForms(): array
{
return [
@@ -113,6 +123,53 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->options(fn (User $user) => $user->getAvailableLanguages()),
]),
Tab::make('OAuth')
->icon('tabler-brand-oauth')
->visible(function () {
foreach (config('auth.oauth') as $name => $data) {
if ($data['enabled']) {
return true;
}
}
return false;
})
->schema(function () {
$providers = [];
foreach (config('auth.oauth') as $name => $data) {
if (!$data['enabled']) {
continue;
}
$unlink = array_key_exists($name, $this->getUser()->oauth ?? []);
$providers[] = Action::make("oauth_$name")
->label(($unlink ? 'Unlink ' : 'Link ') . Str::title($name))
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
->color($data['color'])
->action(function (UserUpdateService $updateService) use ($name, $unlink) {
if ($unlink) {
$oauth = auth()->user()->oauth;
unset($oauth[$name]);
$updateService->handle(auth()->user(), ['oauth' => $oauth]);
$this->fillForm();
Notification::make()
->title("OAuth provider '$name' unlinked")
->success()
->send();
} elseif (config("auth.oauth.$name.enabled")) {
redirect(Socialite::with($name)->redirect()->getTargetUrl());
}
});
}
return [Actions::make($providers)];
}),
Tab::make('2FA')
->icon('tabler-shield-lock')
->schema(function (TwoFactorSetupService $setupService) {
@@ -185,7 +242,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
->content(fn () => new HtmlString("
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
"))
->helperText('Setup Key: '. $secret),
->helperText('Setup Key: ' . $secret),
TextInput::make('2facode')
->label('Code')
->requiredWith('2fapassword')
@@ -215,16 +272,25 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
])->headerActions([
Action::make('Create')
->disabled(fn (Get $get) => $get('description') === null)
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab']))
->action(function (Get $get, Action $action, User $user) {
$token = $user->createToken(
$get('description'),
$get('allowed_ips'),
);
Activity::event('user:api-key.create')
->subject($token->accessToken)
->property('identifier', $token->accessToken->identifier)
->log();
Notification::make()
->title('API Key created')
->body($token->accessToken->identifier . $token->plainTextToken)
->persistent()
->success()
->send();
$action->success();
}),
]),
@@ -289,13 +355,25 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
if ($token = $data['2facode'] ?? null) {
$tokens = $this->toggleTwoFactorService->handle($record, $token, true);
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
cache()->put("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
}
if ($token = $data['2fa-disable-code'] ?? null) {
$this->toggleTwoFactorService->handle($record, $token, false);
try {
$this->toggleTwoFactorService->handle($record, $token, false);
} catch (TwoFactorAuthenticationTokenInvalid $exception) {
Notification::make()
->title('Invalid 2FA Code')
->body($exception->getMessage())
->color('danger')
->icon('tabler-2fa')
->danger()
->send();
throw new Halt();
}
cache()->forget("users.$record->id.2fa.state");
}
@@ -303,18 +381,16 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
return parent::handleRecordUpdate($record, $data);
}
public function exception(Exception $e, Closure $stopPropagation): void
protected function getFormActions(): array
{
if ($e instanceof TwoFactorAuthenticationTokenInvalid) {
Notification::make()
->title('Invalid 2FA Code')
->body($e->getMessage())
->color('danger')
->icon('tabler-2fa')
->danger()
->send();
return [];
}
protected function getHeaderActions(): array
{
return [
$this->getSaveFormAction()->formId('form'),
];
$stopPropagation();
}
}
}

View File

@@ -3,7 +3,13 @@
namespace App\Filament\Pages\Auth;
use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Filament\Pages\Auth\Login as BaseLogin;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class Login extends BaseLogin
{
@@ -13,9 +19,10 @@ class Login extends BaseLogin
'form' => $this->form(
$this->makeForm()
->schema([
$this->getEmailFormComponent(),
$this->getLoginFormComponent(),
$this->getPasswordFormComponent(),
$this->getRememberFormComponent(),
$this->getOAuthFormComponent(),
Turnstile::make('captcha')
->hidden(!config('turnstile.turnstile_enabled'))
->validationMessages([
@@ -31,6 +38,47 @@ class Login extends BaseLogin
{
$this->dispatch('reset-captcha');
parent::throwFailureValidationException();
throw ValidationException::withMessages([
'data.login' => __('filament-panels::pages/auth/login.messages.failed'),
]);
}
protected function getLoginFormComponent(): Component
{
return TextInput::make('login')
->label('Login')
->required()
->autocomplete()
->autofocus()
->extraInputAttributes(['tabindex' => 1]);
}
protected function getOAuthFormComponent(): Component
{
$actions = [];
foreach (config('auth.oauth') as $name => $data) {
if (!$data['enabled']) {
continue;
}
$actions[] = Action::make("oauth_$name")
->label(Str::title($name))
->icon($data['icon'])
->color($data['color'])
->url(route('auth.oauth.redirect', ['driver' => $name], false));
}
return Actions::make($actions);
}
protected function getCredentialsFromFormData(array $data): array
{
$loginType = filter_var($data['login'], FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
return [
$loginType => mb_strtolower($data['login']),
'password' => $data['password'],
];
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Filament\Pages\Installer;
use App\Filament\Pages\Dashboard;
use App\Filament\Admin\Pages\Dashboard;
use App\Filament\Pages\Installer\Steps\CacheStep;
use App\Filament\Pages\Installer\Steps\DatabaseStep;
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
@@ -23,8 +23,6 @@ use Filament\Notifications\Notification;
use Filament\Pages\SimplePage;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Exceptions\Halt;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
@@ -44,7 +42,7 @@ class PanelInstaller extends SimplePage implements HasForms
public function getMaxWidth(): MaxWidth|string
{
return MaxWidth::SevenExtraLarge;
return config('panel.filament.display-width', 'screen-2xl');
}
public static function isInstalled(): bool
@@ -91,20 +89,26 @@ class PanelInstaller extends SimplePage implements HasForms
return 'data';
}
public function submit(UserCreationService $userCreationService): Redirector|RedirectResponse
public function submit(UserCreationService $userCreationService): void
{
// Disable installer
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
try {
// Disable installer
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
// Create admin user & login
$user = $this->createAdminUser($userCreationService);
auth()->guard()->login($user, true);
// Run migrations
$this->runMigrations();
// Write session data at the very end to avoid "page expired" errors
$this->writeToEnv('env_session');
// Create admin user & login
$user = $this->createAdminUser($userCreationService);
auth()->guard()->login($user, true);
// Redirect to admin panel
return redirect(Dashboard::getUrl());
// Write session data at the very end to avoid "page expired" errors
$this->writeToEnv('env_session');
// Redirect to admin panel
$this->redirect(Dashboard::getUrl());
} catch (Halt) {
}
}
public function writeToEnv(string $key): void
@@ -129,13 +133,12 @@ class PanelInstaller extends SimplePage implements HasForms
Artisan::call('config:clear');
}
public function runMigrations(string $driver): void
public function runMigrations(): void
{
try {
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
'--database' => $driver,
]);
} catch (Exception $exception) {
report($exception);

View File

@@ -97,8 +97,6 @@ class DatabaseStep
}
$installer->writeToEnv('env_database');
$installer->runMigrations($driver);
});
}

View File

@@ -57,8 +57,6 @@ class QueueStep
->hidden(fn () => file_exists('/.dockerenv'))
->columnSpanFull(),
])
->afterValidation(function () use ($installer) {
$installer->writeToEnv('env_queue');
});
->afterValidation(fn () => $installer->writeToEnv('env_queue'));
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Filament\Server\Pages;
use App\Filament\Server\Widgets\ServerConsole;
use App\Filament\Server\Widgets\ServerCpuChart;
use App\Filament\Server\Widgets\ServerMemoryChart;
// use App\Filament\Server\Widgets\ServerNetworkChart;
use App\Filament\Server\Widgets\ServerOverview;
use App\Models\Server;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Pages\Page;
use Filament\Support\Enums\ActionSize;
class Console extends Page
{
protected static ?string $navigationIcon = 'tabler-brand-tabler';
protected static ?int $navigationSort = 1;
protected static string $view = 'filament.server.pages.console';
public function getWidgetData(): array
{
return [
'server' => Filament::getTenant(),
'user' => auth()->user(),
];
}
public function getWidgets(): array
{
return [
ServerOverview::class,
ServerConsole::class,
ServerCpuChart::class,
ServerMemoryChart::class,
//ServerNetworkChart::class, TODO: convert units.
];
}
public function getVisibleWidgets(): array
{
return $this->filterVisibleWidgets($this->getWidgets());
}
public function getColumns(): int|string|array
{
return 3;
}
protected function getHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
Action::make('start')
->color('primary')
->size(ActionSize::ExtraLarge)
->action(fn () => $this->dispatch('setServerState', state: 'start'))
->disabled(fn () => $server->isInConflictState()),
Action::make('restart')
->color('gray')
->size(ActionSize::ExtraLarge)
->action(fn () => $this->dispatch('setServerState', state: 'restart'))
->disabled(fn () => $server->isInConflictState() || $server->retrieveStatus() == 'offline'),
Action::make('stop')
->color('danger')
->size(ActionSize::ExtraLarge)
->action(fn () => $this->dispatch('setServerState', state: 'stop'))
->disabled(fn () => $server->isInConflictState() || $server->retrieveStatus() == 'offline'),
Action::make('kill')
->color('danger')
->requiresConfirmation()
->modalHeading('Do you wish to kill this server?')
->modalDescription('This can result in data corruption and/or data loss!')
->modalSubmitActionLabel('Kill Server')
->size(ActionSize::ExtraLarge)
->action(fn () => $this->dispatch('setServerState', state: 'kill'))
->disabled(fn () => $server->isInConflictState() || $server->retrieveStatus() == 'offline'),
];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Filament\Server\Pages;
use App\Models\Server;
use Filament\Facades\Filament;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Form;
use Filament\Pages\Concerns\InteractsWithFormActions;
use Filament\Pages\Page;
/**
* @property Form $form
*/
abstract class ServerFormPage extends Page
{
use InteractsWithFormActions;
use InteractsWithForms;
protected static string $view = 'filament.server.pages.server-form-page';
public ?array $data = [];
public function mount(): void
{
$this->authorizeAccess();
$this->fillForm();
}
protected function authorizeAccess(): void {}
protected function fillForm(): void
{
$data = $this->getRecord()->attributesToArray();
$this->form->fill($data);
}
/**
* @return array<int | string, string | Form>
*/
protected function getForms(): array
{
return [
'form' => $this->form($this->makeForm()
->model($this->getRecord())
->statePath($this->getFormStatePath())
->columns($this->hasInlineLabels() ? 1 : 2)
->inlineLabel($this->hasInlineLabels()),
),
];
}
public function getFormStatePath(): ?string
{
return 'data';
}
public function getRecord(): Server
{
/** @var Server $server */
$server = Filament::getTenant();
return $server;
}
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
}

View File

@@ -0,0 +1,272 @@
<?php
namespace App\Filament\Server\Pages;
use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Support\Enums\Alignment;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Support\Facades\Http;
class Settings extends ServerFormPage
{
protected static ?string $navigationIcon = 'tabler-settings';
protected static ?int $navigationSort = 10;
public function form(Form $form): Form
{
/** @var Server $server */
$server = Filament::getTenant();
return $form
->columns([
'default' => 1,
'sm' => 2,
'md' => 4,
'lg' => 6,
])
->schema([
Section::make('Server Information')
->columns([
'default' => 1,
'sm' => 2,
'md' => 4,
'lg' => 6,
])
->schema([
Fieldset::make('Server')
->label('Information')
->schema([
TextInput::make('name')
->label('Server Name')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
->required()
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 4,
'lg' => 6,
])
->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)),
Textarea::make('description')
->label('Server Description')
->hidden(!config('panel.editable_server_descriptions'))
->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
->columnSpan([
'default' => 1,
'sm' => 2,
'md' => 4,
'lg' => 6,
])
->autosize()
->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
TextInput::make('uuid')
->label('Server UUID')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 3,
'lg' => 5,
])
->disabled(),
TextInput::make('id')
->label('Server ID')
->disabled()
->columnSpan(1),
]),
Fieldset::make('Limits')
->label('Limits')
->columns([
'default' => 1,
'sm' => 1,
'md' => 3,
'lg' => 3,
])
->schema([
TextInput::make('backup_limit')
->label('Backup Limit')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups can be created' : $server->backups->count() . ' of ' . $state),
TextInput::make('database_limit')
->label('Database Limit')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases can be created' : $server->databases->count() . ' of ' . $state),
TextInput::make('allocation_limit')
->label('Allocation Limit')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No additional Allocations can be created' : $server->allocations->count() . ' of ' . $state),
]),
]),
Section::make('Node Information')
->schema([
TextInput::make('node.name')
->label('Node Name')
->formatStateUsing(fn (Server $server) => $server->node->name)
->disabled(),
Fieldset::make('SFTP Information')
->hidden(fn () => !auth()->user()->can(Permission::ACTION_FILE_SFTP, $server))
->label('SFTP Information')
->columns([
'default' => 1,
'sm' => 1,
'md' => 3,
'lg' => 3,
])
->schema([
TextInput::make('connection')
->label('Connection')
->columnSpan(1)
->disabled()
->hintActions([
Action::make('connect_sftp')
->label('Connect to SFTP')
->color('success')
->icon('tabler-plug')
->url(function (Server $server) {
$fqdn = $server->node->daemon_sftp_alias ?? $server->node->fqdn;
return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
}),
])
->formatStateUsing(function (Server $server) {
$fqdn = $server->node->daemon_sftp_alias ?? $server->node->fqdn;
return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
}),
TextInput::make('username')
->label('Username')
->columnSpan(1)
->disabled()
->formatStateUsing(fn (Server $server) => auth()->user()->username . '.' . $server->uuid_short),
Placeholder::make('password')
->columnSpan(1)
->content('Your SFTP password is the same as the password you use to access this panel.'),
]),
]),
Section::make('Reinstall Server')
->hidden(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
->collapsible()->collapsed()
->footerActions([
Action::make('reinstall')
->color('danger')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
->label('Reinstall')
->requiresConfirmation()
->modalHeading('Are you sure you want to reinstall the server?')
->modalDescription('Some files may be deleted or modified during this process, please back up your data before continuing.')
->modalSubmitActionLabel('Yes, Reinstall')
->action(function (Server $server) {
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403);
$server->fill(['status' => ServerState::Installing])->save();
try {
Http::daemon($server->node)->post(sprintf(
'/api/servers/%s/reinstall',
$server->uuid
));
} catch (TransferException $exception) {
throw new DaemonConnectionException($exception);
}
Activity::event('server:settings.reinstall')
->log();
Notification::make()
->success()
->title('Server Reinstall Started')
->send();
}),
])
->footerActionsAlignment(Alignment::Right)
->schema([
Placeholder::make('')
->label('Reinstalling your server will stop it, and then re-run the installation script that initially set it up.'),
Placeholder::make('')
->label('Some files may be deleted or modified during this process, please back up your data before continuing.'),
]),
]);
}
public function updateName(string $name, Server $server): void
{
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server), 403);
$original = $server->name;
try {
$server->forceFill([
'name' => $name,
])->saveOrFail();
if ($original !== $name) {
Activity::event('server:settings.rename')
->property(['old' => $original, 'new' => $name])
->log();
}
Notification::make()
->success()
->duration(5000) // 5 seconds
->title('Updated Server Name')
->body(fn () => $original . ' -> ' . $name)
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title('Failed')
->body($exception->getMessage())
->send();
}
}
public function updateDescription(string $description, Server $server): void
{
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server) && config('panel.editable_server_descriptions'), 403);
$original = $server->description;
try {
$server->forceFill([
'description' => $description,
])->saveOrFail();
if ($original !== $description) {
Activity::event('server:settings.description')
->property(['old' => $original, 'new' => $description])
->log();
}
Notification::make()
->success()
->duration(5000) // 5 seconds
->title('Updated Server Description')
->body(fn () => $original . ' -> ' . $description)
->send();
} catch (Exception $exception) {
Notification::make()
->danger()
->title('Failed')
->body($exception->getMessage())
->send();
}
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace App\Filament\Server\Pages;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Models\ServerVariable;
use Closure;
use Filament\Facades\Filament;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Validator;
class Startup extends ServerFormPage
{
protected static ?string $navigationIcon = 'tabler-player-play';
protected static ?int $navigationSort = 9;
public function form(Form $form): Form
{
/** @var Server $server */
$server = Filament::getTenant();
return $form
->columns([
'default' => 1,
'sm' => 1,
'md' => 4,
'lg' => 6,
])
->schema([
Textarea::make('startup')
->label('Startup Command')
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 4,
])
->autosize()
->readOnly(),
TextInput::make('custom_image')
->label('Docker Image')
->readOnly()
->visible(fn (Server $server) => !in_array($server->image, $server->egg->docker_images))
->formatStateUsing(fn (Server $server) => $server->image)
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 2,
]),
Select::make('image')
->label('Docker Image')
->live()
->visible(fn (Server $server) => in_array($server->image, $server->egg->docker_images))
->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->afterStateUpdated(function ($state, Server $server) {
$original = $server->image;
$server->forceFill(['image' => $state])->saveOrFail();
if ($original !== $server->image) {
Activity::event('server:startup.image')
->property(['old' => $original, 'new' => $state])
->log();
}
Notification::make()
->title('Docker image updated')
->body('Restart the server to use the new image.')
->success()
->send();
})
->options(function (Server $server) {
$images = $server->egg->docker_images;
return array_flip($images);
})
->selectablePlaceholder(false)
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 2,
'lg' => 2,
]),
Section::make('Server Variables')
->schema([
Repeater::make('server_variables')
->label('')
->relationship('viewableServerVariables')
->grid()
->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->reorderable(false)->addable(false)->deletable(false)
->schema(function () {
$text = TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->disabled(fn (ServerVariable $serverVariable) => !$serverVariable->variable->user_editable)
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
->rules([
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $serverVariable->variable->rules,
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
$fail($message);
}
},
]);
$select = Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
->disabled(fn (ServerVariable $serverVariable) => !$serverVariable->variable->user_editable)
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
$components = [$text, $select];
foreach ($components as &$component) {
$component = $component
->live(onBlur: true)
->afterStateUpdated(function ($state, ServerVariable $serverVariable) {
$this->update($state, $serverVariable);
})
->hintIcon('tabler-code')
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
}
return $components;
})
->columnSpan(6),
]),
]);
}
protected function authorizeAccess(): void
{
abort_unless(auth()->user()->can(Permission::ACTION_STARTUP_READ, Filament::getTenant()), 403);
}
public static function canAccess(): bool
{
return auth()->user()->can(Permission::ACTION_STARTUP_READ, Filament::getTenant());
}
private function shouldHideComponent(ServerVariable $serverVariable, Component $component): bool
{
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
if ($component instanceof Select) {
return !$containsRuleIn;
}
if ($component instanceof TextInput) {
return $containsRuleIn;
}
throw new \Exception('Component type not supported: ' . $component::class);
}
private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
{
$inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'));
return str($inRule)
->after('in:')
->explode(',')
->each(fn ($value) => str($value)->trim())
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
public function update(?string $state, ServerVariable $serverVariable): null
{
$original = $serverVariable->variable_value;
try {
$validator = Validator::make(
['variable_value' => $state],
['variable_value' => $serverVariable->variable->rules]
);
if ($validator->fails()) {
Notification::make()
->danger()
->title('Validation Failed: ' . $serverVariable->variable->name)
->body(implode(', ', $validator->errors()->all()))
->send();
return null;
}
ServerVariable::query()->updateOrCreate([
'server_id' => $this->getRecord()->id,
'variable_id' => $serverVariable->variable->id,
], [
'variable_value' => $state ?? '',
]);
if ($original !== $state) {
Activity::event('server:startup.edit')
->property([
'variable' => $serverVariable->variable->env_variable,
'old' => $original,
'new' => $state,
])
->log();
}
Notification::make()
->success()
->title('Updated: ' . $serverVariable->variable->name)
->body(fn () => $original . ' -> ' . $state)
->send();
} catch (\Exception $e) {
Notification::make()
->danger()
->title('Failed: ' . $serverVariable->variable->name)
->body($e->getMessage())
->send();
}
return null;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\ActivityResource\Pages;
use App\Models\ActivityLog;
use App\Models\Permission;
use App\Models\Role;
use App\Models\Server;
use App\Models\User;
use Filament\Facades\Filament;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
class ActivityResource extends Resource
{
protected static ?string $model = ActivityLog::class;
protected static ?string $label = 'Activity';
protected static ?string $pluralLabel = 'Activity';
protected static ?int $navigationSort = 8;
protected static ?string $navigationIcon = 'tabler-stack';
public static function getEloquentQuery(): Builder
{
/** @var Server $server */
$server = Filament::getTenant();
return $server->activity()
->getQuery()
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
// We could do this with a query and a lot of joins, but that gets pretty
// painful so for now we'll execute a simpler query.
$subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]);
$rootAdmins = Role::getRootAdmin()->users()->pluck('id');
$builder->select('activity_logs.*')
->leftJoin('users', function (JoinClause $join) {
$join->on('users.id', 'activity_logs.actor_id')
->where('activity_logs.actor_type', (new User())->getMorphClass());
})
->where(function (Builder $builder) use ($subusers, $rootAdmins) {
$builder->whereNull('users.id')
->orWhereNotIn('users.id', $rootAdmins)
->orWhereIn('users.id', $subusers);
});
});
}
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
}
public static function getPages(): array
{
return [
'index' => Pages\ListActivities::route('/'),
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Filament\Server\Resources\ActivityResource\Pages;
use App\Filament\Server\Resources\ActivityResource;
use App\Models\ActivityLog;
use App\Models\User;
use App\Tables\Columns\DateTimeColumn;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ListActivities extends ListRecords
{
protected static string $resource = ActivityResource::class;
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('event')
->html()
->formatStateUsing(fn ($state, ActivityLog $activityLog) => __('activity.'.str($state)->replace(':', '.'))) // TODO: convert properties to a format that trans likes, see ActivityLogEntry.tsx - wrapProperties
->description(fn ($state) => $state),
TextColumn::make('user')
->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System')
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? route('filament.admin.resources.users.edit', ['record' => $activityLog->actor]) : ''),
DateTimeColumn::make('timestamp')
->since()
->sortable(),
])
->defaultSort('timestamp', 'desc');
}
public function getBreadcrumbs(): array
{
return [];
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\AllocationResource\Pages;
use App\Models\Allocation;
use App\Models\Permission;
use App\Models\Server;
use Filament\Facades\Filament;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
class AllocationResource extends Resource
{
protected static ?string $model = Allocation::class;
protected static ?string $label = 'Network';
protected static ?string $pluralLabel = 'Network';
protected static ?int $navigationSort = 7;
protected static ?string $navigationIcon = 'tabler-network';
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_ALLOCATION_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant());
}
public static function getPages(): array
{
return [
'index' => Pages\ListAllocations::route('/'),
];
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Filament\Server\Resources\AllocationResource\Pages;
use App\Facades\Activity;
use App\Filament\Server\Resources\AllocationResource;
use App\Models\Allocation;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Allocations\FindAssignableAllocationService;
use Filament\Actions;
use Filament\Facades\Filament;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DetachAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
class ListAllocations extends ListRecords
{
protected static string $resource = AllocationResource::class;
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->columns([
TextColumn::make('ip')
->label('Address')
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
TextColumn::make('alias')
->hidden(),
TextColumn::make('port'),
TextInputColumn::make('notes')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
->label('Notes')
->placeholder('No Notes'),
IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled',
default => 'tabler-star',
})
->color(fn ($state) => match ($state) {
true => 'warning',
default => 'gray',
})
->action(function (Allocation $allocation) use ($server) {
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
return $server->update(['allocation_id' => $allocation->id]);
}
})
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label('Primary'),
])
->actions([
DetachAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label('Delete')
->icon('tabler-trash')
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
'notes' => null,
'server_id' => null,
]);
Activity::event('server:allocation.delete')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
}),
]);
}
protected function getHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
Actions\Action::make('addAllocation')
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
->hidden(fn () => !config('panel.client_features.allocations.enabled'))
->disabled(fn () => $server->allocations()->count() >= $server->allocation_limit)
->color(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'danger' : 'primary')
->action(function (FindAssignableAllocationService $service) use ($server) {
$allocation = $service->handle($server);
Activity::event('server:allocation.create')
->subject($allocation)
->property('allocation', $allocation->toString())
->log();
}),
];
}
public function getBreadcrumbs(): array
{
return [];
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\BackupResource\Pages;
use App\Models\Backup;
use App\Models\Permission;
use App\Models\Server;
use Filament\Facades\Filament;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
class BackupResource extends Resource
{
protected static ?string $model = Backup::class;
protected static ?int $navigationSort = 3;
protected static ?string $navigationIcon = 'tabler-file-zip';
protected static bool $canCreateAnother = false;
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_BACKUP_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return auth()->user()->can(Permission::ACTION_BACKUP_CREATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant());
}
public static function getPages(): array
{
return [
'index' => Pages\ListBackups::route('/'),
];
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace App\Filament\Server\Resources\BackupResource\Pages;
use App\Enums\ServerState;
use App\Facades\Activity;
use App\Filament\Server\Resources\BackupResource;
use App\Http\Controllers\Api\Client\Servers\BackupController;
use App\Models\Backup;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonBackupRepository;
use App\Services\Backups\DownloadLinkService;
use App\Services\Backups\InitiateBackupService;
use App\Tables\Columns\BytesColumn;
use App\Tables\Columns\DateTimeColumn;
use Filament\Actions;
use Filament\Facades\Filament;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Http\Request;
class ListBackups extends ListRecords
{
protected static string $resource = BackupResource::class;
protected static bool $canCreateAnother = false;
public function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->label('Name')
->columnSpanFull()
->required(),
TextArea::make('ignored')
->columnSpanFull()
->label('Ignored Files & Directories'),
Toggle::make('is_locked')
->label('Lock?')
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
]);
}
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->columns([
TextColumn::make('name')
->searchable(),
BytesColumn::make('bytes')
->label('Size'),
DateTimeColumn::make('created_at')
->label('Created')
->since()
->sortable(),
IconColumn::make('is_successful')
->label('Successful')
->boolean(),
IconColumn::make('is_locked')
->label('Lock Status')
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock-open' : 'tabler-lock'),
])
->actions([
ActionGroup::make([
Action::make('lock')
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup)),
Action::make('download')
->color('primary')
->icon('tabler-download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true),
Action::make('restore')
->color('success')
->icon('tabler-folder-up')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
->form([
Placeholder::make('')
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
Checkbox::make('truncate')
->label('Delete all files before restoring backup?'),
])
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
if (!is_null($server->status)) {
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This server is not currently in a state that allows for a backup to be restored.')
->send();
}
if (!$backup->is_successful && is_null($backup->completed_at)) { //TODO Change to Notifications
return Notification::make()
->danger()
->title('Backup Restore Failed')
->body('This backup cannot be restored at this time: not completed or failed.')
->send();
}
$log = Activity::event('server:backup.restore')
->subject($backup)
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
// If the backup is for an S3 file we need to generate a unique Download link for
// it that will allow daemon to actually access the file.
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
$url = $downloadLinkService->handle($backup, auth()->user());
}
// Update the status right away for the server so that we know not to allow certain
// actions against it via the Panel API.
$server->update(['status' => ServerState::RestoringBackup]);
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
});
return Notification::make()
->title('Restoring Backup')
->send();
}),
DeleteAction::make('delete')
->disabled(fn (Backup $backup): bool => $backup->is_locked)
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
->modalSubmitActionLabel('Delete Backup')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup)),
]),
]);
}
protected function getHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
Actions\CreateAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
->createAnother(false)
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
$action = $initiateBackupService->setIgnoredFiles(explode(PHP_EOL, $data['ignored'] ?? ''));
if (auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
$action->setIsLocked((bool) $data['is_locked']);
}
$backup = $action->handle($server, $data['name']);
Activity::event('server:backup.start')
->subject($backup)
->property(['name' => $backup->name, 'locked' => (bool) $data['is_locked']])
->log();
return Notification::make()
->title('Backup Created')
->body($backup->name . ' created.')
->success()
->send();
}),
];
}
public function getBreadcrumbs(): array
{
return [];
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\DatabaseResource\Pages;
use App\Models\Database;
use App\Models\Permission;
use App\Models\Server;
use Filament\Facades\Filament;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
class DatabaseResource extends Resource
{
protected static ?string $model = Database::class;
protected static ?int $navigationSort = 6;
protected static ?string $navigationIcon = 'tabler-database';
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_DATABASE_READ, Filament::getTenant());
}
public static function canView(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_DATABASE_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return auth()->user()->can(Permission::ACTION_DATABASE_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant());
}
public static function getPages(): array
{
return [
'index' => Pages\ListDatabases::route('/'),
];
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
use App\Filament\Server\Resources\DatabaseResource;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Databases\DatabaseManagementService;
use App\Services\Databases\DatabasePasswordService;
use App\Tables\Columns\DateTimeColumn;
use Filament\Actions\CreateAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class ListDatabases extends ListRecords
{
protected static string $resource = DatabaseResource::class;
public function form(Form $form): Form
{
/** @var Server $server */
$server = Filament::getTenant();
return $form
->schema([
TextInput::make('database')
->columnSpanFull()
->suffixAction(CopyAction::make()),
TextInput::make('username')
->suffixAction(CopyAction::make()),
TextInput::make('password')
->password()->revealable()
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->hintAction(
Action::make('rotate')
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
->icon('tabler-refresh')
->requiresConfirmation()
->action(function (DatabasePasswordService $service, Database $database, $set, $get) {
$newPassword = $service->handle($database);
$set('password', $newPassword);
$set('JDBC', 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database'));
})
)
->suffixAction(CopyAction::make())
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->label('Connections From'),
TextInput::make('max_connections')
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
TextInput::make('JDBC')
->label('JDBC Connection String')
->password()->revealable()
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->suffixAction(CopyAction::make())
->columnSpanFull()
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('database'),
TextColumn::make('username'),
TextColumn::make('remote'),
DateTimeColumn::make('created_at')
->sortable(),
])
->actions([
ViewAction::make()
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
DeleteAction::make(),
]);
}
protected function getHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
CreateAction::make('new')
->label(fn () => $server->databases()->count() >= $server->database_limit ? 'Database limit reached' : 'Create Database')
->disabled(fn () => $server->databases()->count() >= $server->database_limit)
->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary')
->createAnother(false)
->form([
Grid::make()
->columns(2)
->schema([
Select::make('database_host_id')
->label('Database Host')
->columnSpan(2)
->required()
->placeholder('Select Database Host')
->options(fn () => $server->node->databaseHosts->mapWithKeys(fn (DatabaseHost $databaseHost) => [$databaseHost->id => $databaseHost->name])),
TextInput::make('database')
->columnSpan(1)
->label('Database Name')
->prefix('s'. $server->id . '_')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Leaving this blank will auto generate a random name'),
TextInput::make('remote')
->columnSpan(1)
->label('Connections From')
->default('%'),
]),
])
->action(function ($data, DatabaseManagementService $service) use ($server) {
if (empty($data['database'])) {
$data['database'] = str_random(12);
}
$data['database'] = 's'. $server->id . '_' . $data['database'];
$service->create($server, $data);
}),
];
}
public function getBreadcrumbs(): array
{
return [];
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\FileResource\Pages;
use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use Filament\Facades\Filament;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
class FileResource extends Resource
{
protected static ?string $model = File::class;
protected static ?int $navigationSort = 2;
protected static ?string $navigationIcon = 'tabler-files';
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_FILE_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return auth()->user()->can(Permission::ACTION_FILE_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_FILE_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_FILE_DELETE, Filament::getTenant());
}
public static function getPages(): array
{
return [
'edit' => Pages\EditFiles::route('/edit/{path}'),
'search' => Pages\SearchFiles::route('/search/{searchTerm}'), // TODO: find better way?
'index' => Pages\ListFiles::route('/{path?}'),
];
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Filament\Server\Resources\FileResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Enums\EditorLanguages;
use App\Facades\Activity;
use App\Filament\Server\Resources\FileResource;
use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\Concerns\InteractsWithFormActions;
use Filament\Panel;
use Filament\Resources\Pages\Page;
use Filament\Resources\Pages\PageRegistration;
use Filament\Support\Enums\Alignment;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;
use Livewire\Attributes\Locked;
/**
* @property Form $form
*/
class EditFiles extends Page
{
use HasUnsavedDataChangesAlert;
use InteractsWithFormActions;
use InteractsWithForms;
protected static string $resource = FileResource::class;
protected static string $view = 'filament.server.pages.edit-file';
#[Locked]
public string $path;
public ?array $data = [];
public function form(Form $form): Form
{
/** @var Server $server */
$server = Filament::getTenant();
File::get($server, dirname($this->path))->orderByDesc('is_directory')->orderBy('name');
return $form
->schema([
Select::make('lang')
->live()
->label('')
->placeholder('File Language')
->options(EditorLanguages::class)
->hidden() //TODO Fix Dis
->default(function () {
$split = explode('.', $this->path);
return end($split);
}),
Section::make('Editing: ' . $this->path)
->footerActions([
Action::make('save')
->label('Save Changes')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->icon('tabler-device-floppy')
->keyBindings('mod+s')
->action(function () use ($server) {
$data = $this->form->getState();
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->putContent($this->path, $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', $this->path)
->log();
Notification::make()
->success()
->duration(5000) // 5 seconds
->title('Saved File')
->body(fn () => $this->path)
->send();
}),
Action::make('cancel')
->label('Cancel')
->color('danger')
->icon('tabler-x')
->url(fn () => ListFiles::getUrl(['path' => dirname($this->path)])),
])
->footerActionsAlignment(Alignment::End)
->schema([
MonacoEditor::make('editor')
->label('')
->placeholderText('')
->formatStateUsing(function () use ($server) {
// @phpstan-ignore-next-line
return app(DaemonFileRepository::class)
->setServer($server)
->getContent($this->path, config('panel.files.max_edit_size'));
})
->language(fn (Get $get) => $get('lang') ?? 'plaintext')
->view('filament.plugins.monaco-editor'),
]),
]);
}
public function mount(string $path): void
{
$this->authorizeAccess();
$this->path = $path;
$this->form->fill();
}
protected function authorizeAccess(): void
{
abort_unless(auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, Filament::getTenant()), 403);
}
/**
* @return array<int | string, string | Form>
*/
protected function getForms(): array
{
return [
'form' => $this->form(static::getResource()::form(
$this->makeForm()
->statePath($this->getFormStatePath())
->columns($this->hasInlineLabels() ? 1 : 2)
->inlineLabel($this->hasInlineLabels()),
)),
];
}
public function getFormStatePath(): ?string
{
return 'data';
}
public function getBreadcrumbs(): array
{
$resource = static::getResource();
$breadcrumbs = [
$resource::getUrl() => $resource::getBreadcrumb(),
];
$previousParts = '';
foreach (explode('/', $this->path) as $part) {
$previousParts = $previousParts . '/' . $part;
$breadcrumbs[self::getUrl(['path' => ltrim($previousParts, '/')])] = $part;
}
return $breadcrumbs;
}
public static function route(string $path): PageRegistration
{
return new PageRegistration(
page: static::class,
route: fn (Panel $panel): Route => RouteFacade::get($path, static::class)
->middleware(static::getRouteMiddleware($panel))
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
->where('path', '.*'),
);
}
}

View File

@@ -0,0 +1,588 @@
<?php
namespace App\Filament\Server\Resources\FileResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Enums\EditorLanguages;
use App\Facades\Activity;
use App\Filament\Server\Resources\FileResource;
use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Services\Nodes\NodeJWTService;
use App\Tables\Columns\BytesColumn;
use App\Tables\Columns\DateTimeColumn;
use Carbon\CarbonImmutable;
use Filament\Actions\Action as HeaderAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Panel;
use Filament\Resources\Pages\ListRecords;
use Filament\Resources\Pages\PageRegistration;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\BulkAction;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\Route as RouteFacade;
use Livewire\Attributes\Locked;
class ListFiles extends ListRecords
{
protected static string $resource = FileResource::class;
#[Locked]
public string $path;
public function mount(?string $path = null): void
{
parent::mount();
$this->path = $path ?? '/';
}
public function getBreadcrumbs(): array
{
$resource = static::getResource();
$breadcrumbs = [
$resource::getUrl() => $resource::getBreadcrumb(),
];
$previousParts = '';
foreach (explode('/', $this->path) as $part) {
$previousParts = $previousParts . '/' . $part;
$breadcrumbs[self::getUrl(['path' => ltrim($previousParts, '/')])] = $part;
}
return $breadcrumbs;
}
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->paginated([15, 25, 50, 100])
->defaultPaginationPageOption(15)
->query(fn () => File::get($server, $this->path)->orderByDesc('is_directory')->orderBy('name'))
->columns([
TextColumn::make('name')
->searchable()
->icon(fn (File $file) => $file->getIcon()),
BytesColumn::make('size'),
DateTimeColumn::make('modified_at')
->since()
->sortable(),
])
->recordUrl(function (File $file) use ($server) {
if ($file->is_directory) {
return self::getUrl(['path' => join_paths($this->path, $file->name)]);
}
if (!auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) {
return null;
}
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
})
->actions([
Action::make('view')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->label('Open')
->icon('tabler-eye')
->visible(fn (File $file) => $file->is_directory)
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
EditAction::make('edit')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->label('Edit')
->icon('tabler-edit')
->visible(fn (File $file) => $file->canEdit())
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
ActionGroup::make([
Action::make('rename')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label('Rename')
->icon('tabler-forms')
->form([
TextInput::make('name')
->label('File name')
->default(fn (File $file) => $file->name)
->required(),
])
->action(function ($data, File $file) use ($server) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->renameFiles($this->path, [['to' => $data['name'], 'from' => $file->name]]);
Activity::event('server:file.rename')
->property('directory', $this->path)
->property('files', [['to' => $data['name'], 'from' => $file->name]])
->log();
Notification::make()
->title('File Renamed')
->body(fn () => $file->name . ' -> ' . $data['name'])
->success()
->send();
}),
Action::make('copy')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label('Copy')
->icon('tabler-copy')
->visible(fn (File $file) => $file->is_file)
->action(function (File $file) use ($server) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->copyFile(join_paths($this->path, $file->name));
Activity::event('server:file.copy')
->property('file', join_paths($this->path, $file->name))
->log();
Notification::make()
->title('File copied')
->success()
->send();
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
Action::make('download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->label('Download')
->icon('tabler-download')
->visible(fn (File $file) => $file->is_file)
->action(function (File $file) use ($server) {
// @phpstan-ignore-next-line
$token = app(NodeJWTService::class)
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setUser(auth()->user())
->setClaims([
'file_path' => rawurldecode(join_paths($this->path, $file->name)),
'server_uuid' => $server->uuid,
])
->handle($server->node, auth()->user()->id . $server->uuid);
Activity::event('server:file.download')
->property('file', join_paths($this->path, $file->name))
->log();
return redirect()->away(sprintf('%s/download/file?token=%s', $server->node->getConnectionAddress(), $token->toString())); // TODO: download works, but breaks modals
}),
Action::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label('Move')
->icon('tabler-replace')
->form([
TextInput::make('location')
->label('File name')
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
->default(fn (File $file) => $file->name)
->required()
->live(),
Placeholder::make('new_location')
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location')))),
])
->action(function ($data, File $file) use ($server) {
$location = resolve_path(join_paths($this->path, $data['location']));
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->renameFiles($this->path, [['to' => $location, 'from' => $file->name]]);
Activity::event('server:file.rename')
->property('directory', $this->path)
->property('files', [['to' => $location, 'from' => $file->name]])
->log();
Notification::make()
->title(join_paths($this->path, $file->name) . ' was moved to ' . $location)
->success()
->send();
}),
Action::make('permissions')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label('Permissions')
->icon('tabler-license')
->form([
CheckboxList::make('owner')
->bulkToggleable()
->options([
'read' => 'Read',
'write' => 'Write',
'execute' => 'Execute',
])
->formatStateUsing(function ($state, File $file) {
$mode = (int) substr((string) $file->mode_bits, 0, 1);
return $this->getPermissionsFromModeBit($mode);
}),
CheckboxList::make('group')
->bulkToggleable()
->options([
'read' => 'Read',
'write' => 'Write',
'execute' => 'Execute',
])
->formatStateUsing(function ($state, File $file) {
$mode = (int) substr((string) $file->mode_bits, 1, 1);
return $this->getPermissionsFromModeBit($mode);
}),
CheckboxList::make('public')
->bulkToggleable()
->options([
'read' => 'Read',
'write' => 'Write',
'execute' => 'Execute',
])
->formatStateUsing(function ($state, File $file) {
$mode = (int) substr((string) $file->mode_bits, 2, 1);
return $this->getPermissionsFromModeBit($mode);
}),
])
->action(function ($data, File $file) use ($server) {
$owner = (in_array('read', $data['owner']) ? 4 : 0) | (in_array('write', $data['owner']) ? 2 : 0) | (in_array('execute', $data['owner']) ? 1 : 0);
$group = (in_array('read', $data['group']) ? 4 : 0) | (in_array('write', $data['group']) ? 2 : 0) | (in_array('execute', $data['group']) ? 1 : 0);
$public = (in_array('read', $data['public']) ? 4 : 0) | (in_array('write', $data['public']) ? 2 : 0) | (in_array('execute', $data['public']) ? 1 : 0);
$mode = $owner . $group . $public;
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
Notification::make()
->title('Permissions changed to ' . $mode)
->success()
->send();
}),
Action::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->label('Archive')
->icon('tabler-archive')
->action(function (File $file) use ($server) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->compressFiles($this->path, [$file->name]);
Activity::event('server:file.compress')
->property('directory', $this->path)
->property('files', [$file->name])
->log();
Notification::make()
->title('Archive created')
->success()
->send();
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
Action::make('unarchive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->label('Unarchive')
->icon('tabler-archive')
->visible(fn (File $file) => $file->isArchive())
->action(function (File $file) use ($server) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->decompressFile($this->path, $file->name);
Activity::event('server:file.decompress')
->property('directory', $this->path)
->property('files', $file->name)
->log();
Notification::make()
->title('Unarchive completed')
->success()
->send();
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
]),
DeleteAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->label('')
->icon('tabler-trash')
->requiresConfirmation()
->modalDescription(fn (File $file) => $file->name)
->modalHeading('Delete file?')
->action(function (File $file) use ($server) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->deleteFiles($this->path, [$file->name]);
Activity::event('server:file.delete')
->property('directory', $this->path)
->property('files', $file->name)
->log();
}),
])
->bulkActions([
BulkActionGroup::make([
BulkAction::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->form([
TextInput::make('location')
->label('File name')
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
->default(fn (File $file) => $file->name)
->required()
->live(),
Placeholder::make('new_location')
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
])
->action(function (Collection $files, $data) use ($server) {
$location = resolve_path(join_paths($this->path, $data['location']));
// @phpstan-ignore-next-line
$files = $files->map(fn ($file) => ['to' => $location, 'from' => $file->name])->toArray();
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->renameFiles($this->path, $files);
Activity::event('server:file.rename')
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title(count($files) . ' Files were moved from to ' . $location)
->success()
->send();
}),
BulkAction::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->action(function (Collection $files) use ($server) {
// @phpstan-ignore-next-line
$files = $files->map(fn ($file) => $file->name)->toArray();
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->compressFiles($this->path, $files);
Activity::event('server:file.compress')
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title('Archive created')
->success()
->send();
return redirect(ListFiles::getUrl(['path' => $this->path]));
}),
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
->action(function (Collection $files) use ($server) {
// @phpstan-ignore-next-line
$files = $files->map(fn ($file) => $file->name)->toArray();
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->deleteFiles($this->path, $files);
Activity::event('server:file.delete')
->property('directory', $this->path)
->property('files', $files)
->log();
Notification::make()
->title(count($files) . ' Files deleted.')
->success()
->send();
}),
]),
]);
}
protected function getHeaderActions(): array
{
/** @var Server $server */
$server = Filament::getTenant();
return [
HeaderAction::make('new_file')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label('New File')
->color('gray')
->keyBindings('')
->modalSubmitActionLabel('Create')
->action(function ($data) use ($server) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->putContent(join_paths($this->path, $data['name']), $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', join_paths($this->path, $data['name']))
->log();
})
->form([
TextInput::make('name')
->label('File Name')
->required(),
Select::make('lang')
->live()
->hidden() //TODO: Make file language selection work
->label('Language')
->placeholder('File Language')
->options(EditorLanguages::class),
MonacoEditor::make('editor')
->label('')
->view('filament.plugins.monaco-editor')
->language(fn (Get $get) => $get('lang')),
]),
HeaderAction::make('new_folder')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label('New Folder')
->color('gray')
->action(function ($data) use ($server) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->createDirectory($data['name'], $this->path);
Activity::event('server:file.write')
->property('file', join_paths($this->path, $data['name']))
->log();
})
->form([
TextInput::make('name')
->label('Folder Name')
->required(),
]),
HeaderAction::make('upload')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label('Upload')
->action(function ($data) use ($server) {
if (count($data['files']) > 0 && !isset($data['url'])) {
/** @var UploadedFile $file */
foreach ($data['files'] as $file) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
Activity::event('server:file.uploaded')
->property('directory', $this->path)
->property('file', $file->getFilename())
->log();
}
} elseif ($data['url'] !== null) {
// @phpstan-ignore-next-line
app(DaemonFileRepository::class)
->setServer($server)
->pull($data['url'], $this->path);
Activity::event('server:file.pull')
->property('url', $data['url'])
->property('directory', $this->path)
->log();
}
return redirect(ListFiles::getUrl(['path' => $this->path]));
})
->form([
Tabs::make()
->contained(false)
->schema([
Tabs\Tab::make('Upload Files')
->live()
->schema([
FileUpload::make('files')
->label('File(s)')
->storeFiles(false)
->previewable(false)
->preserveFilenames()
->multiple(),
]),
Tabs\Tab::make('Upload From URL')
->live()
->disabled(fn (Get $get) => count($get('files')) > 0)
->schema([
TextInput::make('url')
->label('URL')
->url(),
]),
]),
]),
HeaderAction::make('search')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->label('Global Search')
->modalSubmitActionLabel('Search')
->form([
TextInput::make('searchTerm')
->placeholder('Enter a search term, e.g. *.txt')
->minLength(3),
])
->action(fn ($data) => redirect(SearchFiles::getUrl([
'searchTerm' => $data['searchTerm'],
'path' => $this->path,
]))),
];
}
public static function route(string $path): PageRegistration
{
return new PageRegistration(
page: static::class,
route: fn (Panel $panel): Route => RouteFacade::get($path, static::class)
->middleware(static::getRouteMiddleware($panel))
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
->where('path', '.*'),
);
}
private function getPermissionsFromModeBit(int $mode): array
{
if ($mode === 1) {
return ['execute'];
} elseif ($mode === 2) {
return ['write'];
} elseif ($mode === 3) {
return ['write', 'execute'];
} elseif ($mode === 4) {
return ['read'];
} elseif ($mode === 5) {
return ['read', 'execute'];
} elseif ($mode === 6) {
return ['read', 'write'];
} elseif ($mode === 7) {
return ['read', 'write', 'execute'];
}
return [];
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Filament\Server\Resources\FileResource\Pages;
use App\Filament\Server\Resources\FileResource;
use App\Models\File;
use App\Models\Server;
use App\Tables\Columns\BytesColumn;
use App\Tables\Columns\DateTimeColumn;
use Filament\Facades\Filament;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Livewire\Attributes\Locked;
class SearchFiles extends ListRecords
{
protected static string $resource = FileResource::class;
protected static ?string $title = 'Global Search';
#[Locked]
public string $searchTerm;
#[Locked]
public string $path;
public function mount(?string $searchTerm = null, ?string $path = null): void
{
parent::mount();
$this->searchTerm = $searchTerm;
$this->path = $path ?? '/';
}
public function getBreadcrumbs(): array
{
$resource = static::getResource();
return [
$resource::getUrl() => $resource::getBreadcrumb(),
self::getUrl(['searchTerm' => $this->searchTerm]) => 'Search "' . $this->searchTerm . '"',
];
}
public function table(Table $table): Table
{
/** @var Server $server */
$server = Filament::getTenant();
return $table
->paginated(false)
->query(fn () => File::get($server, $this->path, $this->searchTerm)->orderByDesc('is_directory')->orderBy('name'))
->columns([
TextColumn::make('name')
->searchable()
->icon(fn (File $file) => $file->getIcon()),
BytesColumn::make('size'),
DateTimeColumn::make('modified_at')
->since()
->sortable(),
])
->recordUrl(function (File $file) {
if ($file->is_directory) {
return ListFiles::getUrl(['path' => join_paths($this->path, $file->name)]);
}
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
});
}
}

View File

@@ -0,0 +1,269 @@
<?php
namespace App\Filament\Server\Resources;
use App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
use App\Models\Permission;
use App\Models\Schedule;
use App\Models\Server;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
class ScheduleResource extends Resource
{
protected static ?string $model = Schedule::class;
protected static ?int $navigationSort = 4;
protected static ?string $navigationIcon = 'tabler-clock';
// TODO: find better way handle server conflict state
public static function canAccess(): bool
{
/** @var Server $server */
$server = Filament::getTenant();
if ($server->isInConflictState()) {
return false;
}
return parent::canAccess();
}
public static function canViewAny(): bool
{
return auth()->user()->can(Permission::ACTION_SCHEDULE_READ, Filament::getTenant());
}
public static function canCreate(): bool
{
return auth()->user()->can(Permission::ACTION_SCHEDULE_CREATE, Filament::getTenant());
}
public static function canEdit(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant());
}
public static function canDelete(Model $record): bool
{
return auth()->user()->can(Permission::ACTION_SCHEDULE_DELETE, Filament::getTenant());
}
public static function form(Form $form): Form
{
return $form
->columns(10)
->schema([
TextInput::make('name')
->columnSpan(10)
->label('Schedule Name')
->placeholder('A human readable identifier for this schedule.')
->autocomplete(false)
->required(),
Toggle::make('only_when_online')
->label('Only when Server is Online?')
->hintIconTooltip('Only execute this schedule when the server is in a running state.')
->hintIcon('tabler-question-mark')
->columnSpan(5)
->required()
->default(1),
Toggle::make('is_active')
->label('Enable Schedule?')
->hintIconTooltip('This schedule will be executed automatically if enabled.')
->hintIcon('tabler-question-mark')
->columnSpan(5)
->required()
->default(1),
TextInput::make('cron_minute')
->columnSpan(2)
->label('Minute')
->default('*/5')
->required(),
TextInput::make('cron_hour')
->columnSpan(2)
->label('Hour')
->default('*')
->required(),
TextInput::make('cron_day_of_month')
->columnSpan(2)
->label('Day of Month')
->default('*')
->required(),
TextInput::make('cron_month')
->columnSpan(2)
->label('Month')
->default('*')
->required(),
TextInput::make('cron_day_of_week')
->columnSpan(2)
->label('Day of Week')
->default('*')
->required(),
Section::make('Presets')
->hiddenOn('view')
->schema([
Actions::make([
Action::make('hourly')
->disabled(fn (string $operation) => $operation === 'view')
->action(function (Set $set) {
$set('cron_minute', '0');
$set('cron_hour', '*');
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
Action::make('daily')
->disabled(fn (string $operation) => $operation === 'view')
->action(function (Set $set) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
Action::make('weekly')
->disabled(fn (string $operation) => $operation === 'view')
->action(function (Set $set) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', '0');
}),
Action::make('monthly')
->disabled(fn (string $operation) => $operation === 'view')
->action(function (Set $set) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '1');
$set('cron_month', '*');
$set('cron_day_of_week', '0');
}),
Action::make('every_x_minutes')
->disabled(fn (string $operation) => $operation === 'view')
->form([
TextInput::make('x')
->label('')
->numeric()
->minValue(1)
->maxValue(60)
->prefix('Every')
->suffix('Minutes'),
])
->action(function (Set $set, $data) {
$set('cron_minute', '*/' . $data['x']);
$set('cron_hour', '*');
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
Action::make('every_x_hours')
->disabled(fn (string $operation) => $operation === 'view')
->form([
TextInput::make('x')
->label('')
->numeric()
->minValue(1)
->maxValue(24)
->prefix('Every')
->suffix('Hours'),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');
$set('cron_hour', '*/' . $data['x']);
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
Action::make('every_x_days')
->disabled(fn (string $operation) => $operation === 'view')
->form([
TextInput::make('x')
->label('')
->numeric()
->minValue(1)
->maxValue(24)
->prefix('Every')
->suffix('Days'),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '*/' . $data['x']);
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
Action::make('every_x_months')
->disabled(fn (string $operation) => $operation === 'view')
->form([
TextInput::make('x')
->label('')
->numeric()
->minValue(1)
->maxValue(24)
->prefix('Every')
->suffix('Months'),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '0');
$set('cron_month', '*/' . $data['x']);
$set('cron_day_of_week', '*');
}),
Action::make('every_x_day_of_week')
->disabled(fn (string $operation) => $operation === 'view')
->form([
Select::make('x')
->label('')
->prefix('Every')
->options([
'0' => 'Sunday',
'1' => 'Monday',
'2' => 'Tuesday',
'3' => 'Wednesday',
'4' => 'Thursday',
'5' => 'Friday',
'6' => 'Saturday',
]),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', $data['x']);
}),
]),
]),
]);
}
public static function getRelations(): array
{
return [
TasksRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSchedules::route('/'),
'create' => Pages\CreateSchedule::route('/create'),
'view' => Pages\ViewSchedule::route('/{record}'),
'edit' => Pages\EditSchedule::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Exceptions\DisplayException;
use App\Filament\Server\Resources\ScheduleResource;
use App\Helpers\Utilities;
use App\Models\Server;
use Carbon\Carbon;
use Exception;
use Filament\Facades\Filament;
use Filament\Resources\Pages\CreateRecord;
class CreateSchedule extends CreateRecord
{
protected static string $resource = ScheduleResource::class;
protected static bool $canCreateAnother = false;
protected function mutateFormDataBeforeCreate(array $data): array
{
if (!isset($data['server_id'])) {
/** @var Server $server */
$server = Filament::getTenant();
$data['server_id'] = $server->id;
}
if (!isset($data['next_run_at'])) {
$data['next_run_at'] = $this->getNextRunAt($data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], $data['cron_day_of_week']);
}
return $data;
}
protected function getNextRunAt(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
{
try {
return Utilities::getScheduleNextRunDate(
$minute,
$hour,
$dayOfMonth,
$month,
$dayOfWeek
);
} catch (Exception) {
throw new DisplayException('The cron data provided does not evaluate to a valid expression.');
}
}
public function getBreadcrumbs(): array
{
return [];
}
}

View File

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

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Schedule;
use App\Tables\Columns\DateTimeColumn;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class ListSchedules extends ListRecords
{
protected static string $resource = ScheduleResource::class;
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('cron')
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
TextColumn::make('status')
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
IconColumn::make('only_when_online')
->boolean()
->sortable(),
DateTimeColumn::make('last_run_at')
->label('Last run')
->placeholder('Never')
->since()
->sortable(),
DateTimeColumn::make('next_run_at')
->label('Next run')
->since()
->sortable(),
])
->actions([
ViewAction::make(),
EditAction::make(),
DeleteAction::make(),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
public function getBreadcrumbs(): array
{
return [];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Server\Resources\ScheduleResource;
use Filament\Actions;
use Filament\Resources\Pages\ViewRecord;
class ViewSchedule extends ViewRecord
{
protected static string $resource = ScheduleResource::class;
protected function getHeaderActions(): array
{
return [
Actions\EditAction::make(),
];
}
public function getBreadcrumbs(): array
{
return [];
}
}

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