Compare commits

...

100 Commits

Author SHA1 Message Date
github-actions[bot]
d39241839c ci(release): bump version 2025-09-04 21:31:53 +00:00
Charles
925ab26fb4 Encode file path in url for folders (#1662) 2025-09-04 17:24:58 -04:00
Charles
2952e22619 Encode file path in url (#1661) 2025-09-04 17:15:46 -04:00
MartinOscar
079eaed010 Fix finish & add translation for Installer title (#1659) 2025-09-04 21:39:10 +02:00
MartinOscar
6671d45651 Fix various Translations & add Installer & add Notifications (#1632) 2025-09-04 20:17:59 +02:00
Boy132
3543b4773a Rename api key prefixes for better clarity (#1650) 2025-09-04 08:43:06 +02:00
IThundxr
02f788a659 Fix auto deploy docker command not including the container argument (#1584)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-09-03 22:30:18 +02:00
Boy132
7ace3978d8 Remove leftovers from activity log batch (#1649) 2025-09-03 22:26:17 +02:00
Boy132
8f277aaca0 Create custom startup variable field (#1615) 2025-09-02 09:05:36 +02:00
SaurFort
76451fa0ad fix: Wrong conversion if decimal prefix selected (#1626)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-08-31 13:51:27 +02:00
Boy132
0104a08ba4 Create custom number format method to catch invalid languages on php 8.4 (#1623) 2025-08-31 13:48:47 +02:00
MartinOscar
5eff006843 Fix activityLog permission name (#1641) 2025-08-31 12:59:48 +02:00
MartinOscar
a8241bf9f3 Fix Installer, Admin & Exit admin redirect (#1640) 2025-08-30 14:37:59 +02:00
MartinOscar
4aae2562ea Update bug-report logs url (#1630) 2025-08-25 12:13:27 +02:00
Boy132
42db5b328a Fix translation for invalid schedule cron + cleanup translations for import modal (#1618) 2025-08-18 23:54:25 +02:00
Boy132
bc4dfb3e92 Fix 500 for closeable alert banners (#1620) 2025-08-18 23:53:59 +02:00
Michael (Parker) Parker
3b9c81534f fix php ini permissions (#1619) 2025-08-17 09:34:41 -05:00
Boy132
f31aa78f6f Fix gap for profile repeaters (api keys, ssh keys, activity logs) (#1613) 2025-08-15 14:07:23 +02:00
Boy132
b5ebd544f4 Improve translation for "link" and "unlink" (oauth) (#1612) 2025-08-15 14:06:53 +02:00
Boy132
c77a37ec89 Fix & cleanup OAuthController (#1599) 2025-08-14 08:29:58 +02:00
Michael (Parker) Parker
4d78e5dcd1 Merge pull request #1609 from parkervcp/add_fcgi_healthcheck
add missing package for healthcheck
2025-08-13 14:15:44 -05:00
Michael (Parker) Parker
15075b6ab8 re-add file server directive 2025-08-13 13:44:21 -05:00
Lance Pioch
a8f233e204 Laravel 12.23.1 Shift (#1604)
Co-authored-by: Shift <shift@laravelshift.com>
2025-08-13 08:01:48 -04:00
Boy132
795cad43b9 Server creation: Only get node_id from allocation if it is missing (#1598) 2025-08-12 15:02:49 -04:00
Charles
46934d7a85 fix eggs with [] (#1596) 2025-08-12 15:02:41 -04:00
Michael (Parker) Parker
06067f375c Add fcgi package for healthcheck
I missed adding the package to the dockerfile so the healthcheck is failing
2025-08-12 09:08:10 -05:00
Charles
d1df53c683 fix lang (#1590) 2025-08-11 18:12:33 -04:00
Charles
b03d2cf919 composer update + update jwt (#1587) 2025-08-11 16:57:59 -04:00
Boy132
27a8423f55 Fix container status caching (#1588) 2025-08-11 22:21:52 +02:00
Michael (Parker) Parker
ad70934430 Update healthcheck (#1571) 2025-08-10 15:30:58 -04:00
Boy132
900f8d0fe1 Cleanup remote api requests (#1579) 2025-08-09 17:53:45 -04:00
Lance Pioch
6a4ac515a7 Laravel 12.22.1 Shift (#1580)
Co-authored-by: Shift <shift@laravelshift.com>
2025-08-09 17:53:29 -04:00
Boy132
7c315ac995 Auto create missing users when using oauth (#1573) 2025-08-07 11:22:30 +02:00
Boy132
49e9440e0f Fix server creation without deployment (#1569) 2025-08-07 11:16:32 +02:00
Alex Smith
02e3e43f1e Update egg-vanilla-minecraft.yaml (#1574)
Co-authored-by: Charles <charles@pelican.dev>
2025-08-05 17:27:00 -04:00
Charles
8eddef6f04 Update minecraft eggs to support ipv4/ipv6 (#1577) 2025-08-05 17:26:49 -04:00
Boy132
d2f1936bbf Add abstract base class for panel providers (#1576) 2025-08-05 23:17:34 +02:00
Charles
36863f94c0 Allow user selectable navigation type (#1572)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-08-05 08:56:31 -04:00
Charles
75863c50d1 Load app.css before filament styles (#1575) 2025-08-04 18:11:34 -04:00
Charles
ec0727b406 Allow eggs to be exported/imported as YAML (#1535) 2025-08-04 07:32:10 -04:00
Boy132
5b2e9d94ca Cleanup and update node packages (#1557) 2025-08-04 11:51:18 +02:00
Charles
8840d109ef Client area translations (#1554) 2025-08-01 07:26:14 -04:00
Boy132
71225bd2dc Refactor AlertBanner to be ViewComponent (#1555) 2025-07-31 23:54:53 +02:00
JoanFo
bab8ec6e18 Fixed not working variables on DiscordWebhooks and headers. (#1516)
Co-authored-by: notCharles <charles@pelican.dev>
2025-07-31 15:47:46 -06:00
Awhikax
d307a2095b Allow for backups to be renamed (#1546) 2025-07-31 15:47:15 -06:00
Hasyirin Fakhriy
a777f4e0ff remove maxlength rule from egg variable's default_value field (#1559) 2025-07-31 15:45:28 -06:00
Boy132
86a71afc6c Cleanup formatResource (#1563) 2025-07-31 23:02:27 +02:00
Hasyirin Fakhriy
88943563c7 Add tags field to eggs transformer. (#1550) 2025-07-22 14:39:18 -04:00
Lance Pioch
20071a64fa Laravel 12.21.0 Shift (#1551)
Co-authored-by: Shift <shift@laravelshift.com>
2025-07-22 14:39:02 -04:00
Charles
d0d3418e03 Move header actions to iconbuttons (#1541) 2025-07-22 12:31:23 -04:00
Boy132
083e3dc62a Update contributing guide (#1548) 2025-07-22 15:45:29 +02:00
Charles
d7e60f2456 Fix Console Fit... again (#1537) 2025-07-19 15:40:18 -04:00
Charles
38e746240d Fix delayed status update, and graphs (#1536) 2025-07-19 14:45:50 -04:00
Lance Pioch
986063dce4 Use default startup variable value when creating server via api (#1518)
Co-authored-by: Boy132 <mail@boy132.de>
2025-07-19 13:58:04 -04:00
Charles
71d0326cb2 Call FitConsole after page load (#1534) 2025-07-19 13:04:22 -04:00
Boy132
62ca53eeaf Server Policy: Only do owner check if checking for subuser permissions (#1521) 2025-07-19 18:52:28 +02:00
Boy132
9f2305f351 Use filaments password broker for reset link token when creating subuser (#1498) 2025-07-19 18:51:42 +02:00
Boy132
340d1b543c Add import & export for schedules (#1530) 2025-07-19 16:48:21 +02:00
Boy132
61098b11f2 Add migration to clear password from auth:fail logs (#1533) 2025-07-19 16:47:49 +02:00
Boy132
4d03d6b948 Improve Mounts API (#1531) 2025-07-18 13:50:31 +02:00
Boy132
1f67054777 Fix phpstan (#1532) 2025-07-18 13:49:26 +02:00
Charles
4a9814f16c Move fullscreen file editor down to not cover top bar (#1527) 2025-07-18 05:05:09 -04:00
Boy132
e0697d3288 Cleanup & fix server deployment (#1497) 2025-07-18 08:23:48 +02:00
Boy132
d165da20ec Improve schedule form (#1514) 2025-07-18 08:23:08 +02:00
Charles
ae27b179fe Fix memory leak caused by shift pr (#1528) 2025-07-17 17:41:41 -04:00
Rain
1113ffe0f7 Filters sensitive credential fields from auth:fail logs (#1504) 2025-07-17 16:45:38 -04:00
Lance Pioch
5531bc0ba1 Laravel 12.20.0 Shift (#1500)
Co-authored-by: Shift <shift@laravelshift.com>
2025-07-17 16:44:27 -04:00
Charles
a3819122db Fix power actions (#1517) 2025-07-15 05:02:55 -04:00
MartinOscar
c5528a61f3 Filter out already used ips with the same port (#1496) 2025-07-10 08:59:46 +02:00
Boy132
5a7c6ac6e5 Improve turnstile error handling (+ cleanup) (#1501) 2025-07-09 13:51:43 +02:00
Boy132
5e8cccef19 Fix options for script_entry Select (#1505) 2025-07-09 09:14:46 +02:00
Charles
0ccb248d91 Add Languages (#1499)
Co-authored-by: Boy132 <mail@boy132.de>
2025-07-08 21:16:11 -04:00
Boy132
514d961c24 Add migration to match node ports (#1489) 2025-07-07 08:37:45 +02:00
Charles
f8e802afcd Fix table view power actions (#1490) 2025-07-06 19:03:09 -04:00
Boy132
556551b4f3 Add SSH Keys to Profile (#1478) 2025-07-06 22:51:45 +02:00
Boy132
23ddded61e Replace gethostbynamel with dns_get_record (#1479) 2025-07-06 22:42:59 +02:00
JoanFo
c5aa8a3980 DiscordWebhooks (#1355)
Co-authored-by: notCharles <charles@pelican.dev>
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
Co-authored-by: Boy132 <mail@boy132.de>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-07-05 12:42:34 -04:00
MartinOscar
21ac75efae Nullable eggFeatures in FeatureService (#1485) 2025-07-05 14:57:08 +02:00
JoanFo
9655700cde Nullable allocation in server-entry blade² (#1486) 2025-07-05 14:25:33 +02:00
JoanFo
c9b7e979c0 Nullable allocation in server-entry blade (#1484) 2025-07-05 14:14:43 +02:00
MartinOscar
77a3b0640d Add dehydratedWhenHidden to serverVariable TextInput & Select (#1476) 2025-07-03 08:55:18 +02:00
pelican-vehikl
de4cb38766 Refactor Providers to be a singleton (#1327) 2025-07-01 21:33:11 -04:00
Charles
74bd7f9991 Move console js to built app.js file. (#1471) 2025-07-01 17:13:44 -04:00
Charles
ba7f814300 back port power actions from v4 branch (#1470) 2025-06-28 10:41:16 -04:00
MartinOscar
cdcd1c521e Add FileExistsException & Fix error reporting (#1417) 2025-06-26 21:04:33 +02:00
Boy132
4d0aabe91e Schedule task improvements (#1468) 2025-06-26 17:00:37 +02:00
Boy132
68f72b9b4d Add "egg index" and dropdown to egg importer (#1451)
Co-authored-by: notCharles <charles@pelican.dev>
2025-06-25 19:50:09 -04:00
JoanFo
dca37ccc95 Server Without Allocations (#1432)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-06-25 19:49:43 -04:00
Charles
6a088d0c4f Tweak Grid View, Use Memory Limit, not wings reported allocation (#1462) 2025-06-25 19:49:00 -04:00
Walter van der Broek
7731f16b0f Fix: Search for tags in correct variable (#1461) 2025-06-25 19:48:39 -04:00
Lance Pioch
9a1e7de4ae Laravel 12.19.3 Shift (#1455)
Co-authored-by: Shift <shift@laravelshift.com>
2025-06-22 15:46:29 -04:00
pelican-vehikl
c61b6920b9 Fix some tests (#1450) 2025-06-19 21:36:50 +02:00
Boy132
6107524522 Trait-ify resources and add customizable options (#1396) 2025-06-19 18:24:25 +02:00
Boy132
57a13a2701 Refactor admin dashboard widgets to use forms (#1452) 2025-06-19 18:23:32 +02:00
Boy132
4dd414ad87 Delete old csgo egg (#1448) 2025-06-19 18:18:06 +02:00
Boy132
0156ac1509 Role icons: Use correct capitalization for class names (#1447) 2025-06-12 20:27:02 +02:00
MartinOscar
387471716b Fully remove the filament-context-menu package (#1449) 2025-06-12 20:26:39 +02:00
Boy132
1dc5ec027e Cleanup & fix server list (#1433) 2025-06-12 08:54:00 +02:00
MartinOscar
b05eabfdb0 Fix Users seeing Open in admin (#1444) 2025-06-11 03:51:08 +02:00
Lance Pioch
3039c1c698 Laravel 12.18.0 Shift (#1443)
Co-authored-by: Shift <shift@laravelshift.com>
2025-06-10 21:48:21 -04:00
683 changed files with 26849 additions and 5158 deletions

View File

@@ -64,10 +64,9 @@ body:
label: Error Logs
description: |
Run the following command to collect logs on your system.
Wings: `sudo wings diagnostics`
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
placeholder: "https://pelipaste.com/a1h6z"
Wings: `sudo wings diagnostics --hastebin-url=https://logs.pelican.dev`
Panel: `tail -n 300 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl --data-binary @- https://logs.pelican.dev`
placeholder: "https://logs.pelican.dev/c17f750e"
render: bash
validations:
required: false

View File

@@ -63,8 +63,8 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
WORKDIR /var/www/html
# Install additional required libraries
RUN apk update && apk add --no-cache \
caddy ca-certificates supervisor supercronic
RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic fcgi
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@@ -85,7 +85,8 @@ RUN chown root:www-data ./ \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
@@ -93,10 +94,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh ./docker/entrypoint.sh
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1
CMD /bin/ash /healthcheck.sh
EXPOSE 80 443
@@ -104,5 +106,5 @@ VOLUME /pelican-data
USER www-data
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@@ -67,8 +67,8 @@ FROM --platform=$TARGETOS/$TARGETARCH base AS final
WORKDIR /var/www/html
# Install additional required libraries
RUN apk update && apk add --no-cache \
caddy ca-certificates supervisor supercronic
RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic fcgi coreutils
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@@ -89,7 +89,8 @@ RUN chown root:www-data ./ \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
@@ -97,10 +98,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh ./docker/entrypoint.sh
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1
CMD /bin/ash /healthcheck.sh
EXPOSE 80 443
@@ -108,5 +110,5 @@ VOLUME /pelican-data
USER www-data
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@@ -2,10 +2,13 @@
namespace App\Console\Commands\Egg;
use App\Enums\EggFormat;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use Exception;
use Illuminate\Console\Command;
use JsonException;
use Symfony\Component\Yaml\Yaml;
class CheckEggUpdatesCommand extends Command
{
@@ -23,6 +26,9 @@ class CheckEggUpdatesCommand extends Command
}
}
/**
* @throws JsonException
*/
private function check(Egg $egg, EggExporterService $exporterService): void
{
if (is_null($egg->update_url)) {
@@ -31,22 +37,26 @@ class CheckEggUpdatesCommand extends Command
return;
}
$currentJson = json_decode($exporterService->handle($egg->id));
unset($currentJson->exported_at);
$ext = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION));
$isYaml = in_array($ext, ['yaml', 'yml']);
$updatedEgg = file_get_contents($egg->update_url);
assert($updatedEgg !== false);
$updatedJson = json_decode($updatedEgg);
unset($updatedJson->exported_at);
$local = $isYaml
? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML))
: json_decode($exporterService->handle($egg->id, EggFormat::JSON), true);
if (md5(json_encode($currentJson, JSON_THROW_ON_ERROR)) === md5(json_encode($updatedJson, JSON_THROW_ON_ERROR))) {
$this->info("$egg->name: Up-to-date");
cache()->put("eggs.$egg->uuid.update", false, now()->addHour());
$remote = file_get_contents($egg->update_url);
assert($remote !== false);
return;
}
$remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
$this->warn("$egg->name: Found update");
cache()->put("eggs.$egg->uuid.update", true, now()->addHour());
unset($local['exported_at'], $remote['exported_at']);
$localHash = md5(json_encode($local, JSON_THROW_ON_ERROR));
$remoteHash = md5(json_encode($remote, JSON_THROW_ON_ERROR));
$status = $localHash === $remoteHash ? 'Up-to-date' : 'Found update';
$this->{($localHash === $remoteHash) ? 'info' : 'warn'}("$egg->name: $status");
cache()->put("eggs.$egg->uuid.update", $localHash !== $remoteHash, now()->addHour());
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands\Egg;
use Exception;
use Illuminate\Console\Command;
class UpdateEggIndexCommand extends Command
{
protected $signature = 'p:egg:update-index';
public function handle(): int
{
try {
$data = file_get_contents('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json');
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} catch (Exception $exception) {
$this->error($exception->getMessage());
return 1;
}
$index = [];
foreach ($data['nests'] as $nest) {
$nestName = $nest['nest_type'];
$this->info("Nest: $nestName");
$nestEggs = [];
foreach ($nest['Eggs'] as $egg) {
$eggName = $egg['egg']['name'];
$this->comment("Egg: $eggName");
$nestEggs[$egg['download_url']] = $eggName;
}
$index[$nestName] = $nestEggs;
$this->info('');
}
cache()->forever('eggs.index', $index);
return 0;
}
}

View File

@@ -64,7 +64,7 @@ class ProcessRunnableCommand extends Command
} catch (Throwable $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]);
$this->error(trans('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
$this->error(trans('commands.schedule.process.error_message', ['schedules' => " #$schedule->id: " . $exception->getMessage()]));
}
}
}

View File

@@ -19,7 +19,7 @@ class DisableTwoFactorCommand extends Command
public function handle(): void
{
if ($this->input->isInteractive()) {
$this->output->warning(trans('command/messages.user.2fa_help_text.0') . trans('command/messages.user.2fa_help_text.1'));
$this->output->warning(trans('command/messages.user.2fa_help_text'));
}
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));

View File

@@ -3,6 +3,7 @@
namespace App\Console;
use App\Console\Commands\Egg\CheckEggUpdatesCommand;
use App\Console\Commands\Egg\UpdateEggIndexCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
@@ -41,7 +42,9 @@ class Kernel extends ConsoleKernel
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
$schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->command(CheckEggUpdatesCommand::class)->daily();
$schedule->command(UpdateEggIndexCommand::class)->daily();
if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.

View File

@@ -32,6 +32,6 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string
{
return str($this->value)->headline();
return trans('server/backup.backup_status.' . $this->value);
}
}

View File

@@ -68,7 +68,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string
{
return str($this->value)->title();
return trans('server/console.status.' . $this->value);
}
public function isOffline(): bool
@@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function isStartable(): bool
{
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]);
}
public function isRestartable(): bool
@@ -97,18 +97,16 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
return true;
}
return !in_array($this, [ContainerStatus::Offline]);
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
}
public function isStoppable(): bool
{
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]);
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]);
}
public function isKillable(): bool
{
// [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created]
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]);
}
}

9
app/Enums/EggFormat.php Normal file
View File

@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum EggFormat: string
{
case YAML = 'yaml';
case JSON = 'json';
}

View File

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

View File

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

View File

@@ -2,9 +2,50 @@
namespace App\Enums;
enum ServerResourceType
use App\Models\Server;
enum ServerResourceType: string
{
case Unit;
case Percentage;
case Time;
case Uptime = 'uptime';
case CPU = 'cpu_absolute';
case Memory = 'memory_bytes';
case Disk = 'disk_bytes';
case CPULimit = 'cpu';
case MemoryLimit = 'memory';
case DiskLimit = 'disk';
/**
* @return int resource amount in bytes
*/
public function getResourceAmount(Server $server): int
{
if ($this->isLimit()) {
$resourceAmount = $server->{$this->value} ?? 0;
if (!$this->isPercentage()) {
// Our limits are entered as MiB/ MB so we need to convert them to bytes
$resourceAmount *= config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000;
}
return $resourceAmount;
}
return $server->retrieveResources()[$this->value] ?? 0;
}
public function isLimit(): bool
{
return $this === ServerResourceType::CPULimit || $this === ServerResourceType::MemoryLimit || $this === ServerResourceType::DiskLimit;
}
public function isTime(): bool
{
return $this === ServerResourceType::Uptime;
}
public function isPercentage(): bool
{
return $this === ServerResourceType::CPU || $this === ServerResourceType::CPULimit;
}
}

View File

@@ -8,7 +8,6 @@ use Filament\Support\Contracts\HasLabel;
enum ServerState: string implements HasColor, HasIcon, HasLabel
{
case Normal = 'normal';
case Installing = 'installing';
case InstallFailed = 'install_failed';
case ReinstallFailed = 'reinstall_failed';
@@ -18,7 +17,6 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
public function getIcon(): string
{
return match ($this) {
self::Normal => 'tabler-heart',
self::Installing => 'tabler-heart-bolt',
self::InstallFailed => 'tabler-heart-x',
self::ReinstallFailed => 'tabler-heart-x',
@@ -27,10 +25,17 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
};
}
public function getColor(): string
public function getColor(bool $hex = false): string
{
if ($hex) {
return match ($this) {
self::Installing, self::RestoringBackup => '#2563EB',
self::Suspended => '#D97706',
self::InstallFailed, self::ReinstallFailed => '#EF4444',
};
}
return match ($this) {
self::Normal => 'primary',
self::Installing => 'primary',
self::InstallFailed => 'danger',
self::ReinstallFailed => 'danger',

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum StartupVariableType: string
{
case Text = 'text';
case Select = 'select';
case Toggle = 'toggle'; // TODO: add toggle to blade view
}

34
app/Enums/WebhookType.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
enum WebhookType: string implements HasColor, HasIcon, HasLabel
{
case Regular = 'regular';
case Discord = 'discord';
public function getLabel(): string
{
return trans('admin/webhook.' . $this->value);
}
public function getColor(): ?string
{
return match ($this) {
self::Regular => null,
self::Discord => 'blurple',
};
}
public function getIcon(): string
{
return match ($this) {
self::Regular => 'tabler-world-www',
self::Discord => 'tabler-brand-discord',
};
}
}

View File

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

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableNodeException extends DisplayException {}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
abstract class AvatarProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
public static function getProvider(string $id): ?self
{
return Arr::get(static::$providers, $id);
}
/**
* @return array<string, static>
*/
public static function getAll(): array
{
return static::$providers;
}
public function __construct()
{
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
abstract public function get(User $user): ?string;
public function getName(): string
{
return Str::title($this->getId());
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
interface AvatarSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function get(User $user): ?string;
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
class AvatarService
{
/** @var AvatarSchemaInterface[] */
private array $schemas = [];
public function __construct(
private readonly bool $allowUploadedAvatars,
private readonly string $activeSchema,
) {}
public function get(string $id): ?AvatarSchemaInterface
{
return array_get($this->schemas, $id);
}
public function getActiveSchema(): ?AvatarSchemaInterface
{
return $this->get($this->activeSchema);
}
public function getAvatarUrl(User $user): ?string
{
if ($this->allowUploadedAvatars) {
$path = "avatars/$user->id.png";
if (Storage::disk('public')->exists($path)) {
return Storage::url($path);
}
}
return $this->getActiveSchema()?->get($user);
}
public function register(AvatarSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/** @return array<string, string> */
public function getMappings(): array
{
return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all();
}
}

View File

@@ -1,24 +1,24 @@
<?php
namespace App\Extensions\Avatar\Providers;
namespace App\Extensions\Avatar\Schemas;
use App\Extensions\Avatar\AvatarProvider;
use App\Extensions\Avatar\AvatarSchemaInterface;
use App\Models\User;
class GravatarProvider extends AvatarProvider
class GravatarSchema implements AvatarSchemaInterface
{
public function getId(): string
{
return 'gravatar';
}
public function getName(): string
{
return 'Gravatar';
}
public function get(User $user): string
{
return 'https://gravatar.com/avatar/' . md5($user->email);
}
public static function register(): self
{
return new self();
}
}

View File

@@ -1,11 +1,11 @@
<?php
namespace App\Extensions\Avatar\Providers;
namespace App\Extensions\Avatar\Schemas;
use App\Extensions\Avatar\AvatarProvider;
use App\Extensions\Avatar\AvatarSchemaInterface;
use App\Models\User;
class UiAvatarsProvider extends AvatarProvider
class UiAvatarsSchema implements AvatarSchemaInterface
{
public function getId(): string
{
@@ -22,9 +22,4 @@ class UiAvatarsProvider extends AvatarProvider
// UI Avatars is the default of filament so just return null here
return null;
}
public static function register(): self
{
return new self();
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Extensions\Captcha;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class CaptchaService
{
/** @var array<string, CaptchaSchemaInterface> */
private array $schemas = [];
/**
* @return CaptchaSchemaInterface[]
*/
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?CaptchaSchemaInterface
{
return array_get($this->schemas, $id);
}
public function register(CaptchaSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
config()->set('captcha.' . Str::lower($schema->getId()), $schema->getConfig());
$this->schemas[$schema->getId()] = $schema;
}
/** @return Collection<CaptchaSchemaInterface> */
public function getActiveSchemas(): Collection
{
return collect($this->schemas)
->filter(fn (CaptchaSchemaInterface $schema) => $schema->isEnabled());
}
public function getActiveSchema(): ?CaptchaSchemaInterface
{
return $this->getActiveSchemas()->first();
}
}

View File

@@ -1,118 +0,0 @@
<?php
namespace App\Extensions\Captcha\Providers;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Illuminate\Foundation\Application;
use Illuminate\Support\Str;
abstract class CaptchaProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @return self|static[]
*/
public static function get(?string $id = null): array|self
{
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate Captcha provider with id '{$this->getId()}'");
}
return;
}
config()->set('captcha.' . Str::lower($this->getId()), $this->getConfig());
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
abstract public function getComponent(): Component;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
$id = Str::upper($this->getId());
return [
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
->label('Site Key')
->placeholder('Site Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SITE_KEY")),
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
->label('Secret Key')
->placeholder('Secret Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
public function getName(): string
{
return Str::title($this->getId());
}
public function getIcon(): ?string
{
return null;
}
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("CAPTCHA_{$id}_ENABLED", false);
}
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array
{
return [
'success' => false,
'message' => 'validateResponse not defined',
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
return true;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Extensions\Captcha\Schemas;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Str;
abstract class BaseSchema
{
abstract public function getId(): string;
public function getName(): string
{
return Str::upper($this->getId());
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
$id = Str::upper($this->getId());
return [
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
->label('Site Key')
->placeholder('Site Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SITE_KEY")),
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
->label('Secret Key')
->placeholder('Secret Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Extensions\Captcha\Schemas;
use Filament\Forms\Components\Component;
interface CaptchaSchemaInterface
{
public function getId(): string;
public function getName(): string;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array;
public function isEnabled(): bool;
public function getFormComponent(): Component;
/**
* @return Component[]
*/
public function getSettingsForm(): array;
public function getIcon(): ?string;
public function validateResponse(?string $captchaResponse = null): void;
}

View File

@@ -1,11 +1,10 @@
<?php
namespace App\Filament\Components\Forms\Fields;
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Rules\ValidTurnstileCaptcha;
use Filament\Forms\Components\Field;
class TurnstileCaptcha extends Field
class Component extends Field
{
protected string $viewIdentifier = 'turnstile';
@@ -19,8 +18,6 @@ class TurnstileCaptcha extends Field
$this->required();
$this->after(function (TurnstileCaptcha $component) {
$component->rule(new ValidTurnstileCaptcha());
});
$this->rule(new Rule());
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\CaptchaService;
use Closure;
use Exception;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\App;
class Rule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
App::call(fn (CaptchaService $service) => $service->get('turnstile')->validateResponse($value));
} catch (Exception $exception) {
report($exception);
$fail('Captcha validation failed: ' . $exception->getMessage());
}
}
}

View File

@@ -1,26 +1,31 @@
<?php
namespace App\Extensions\Captcha\Providers;
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Filament\Components\Forms\Fields\TurnstileCaptcha;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use App\Extensions\Captcha\Schemas\BaseSchema;
use Exception;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Component as BaseComponent;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Toggle;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
class TurnstileProvider extends CaptchaProvider
class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
{
public function getId(): string
{
return 'turnstile';
}
public function getComponent(): Component
public function isEnabled(): bool
{
return TurnstileCaptcha::make('turnstile');
return env('CAPTCHA_TURNSTILE_ENABLED', false);
}
public function getFormComponent(): BaseComponent
{
return Component::make('turnstile');
}
/**
@@ -34,7 +39,7 @@ class TurnstileProvider extends CaptchaProvider
}
/**
* @return Component[]
* @return BaseComponent[]
*/
public function getSettingsForm(): array
{
@@ -52,24 +57,18 @@ class TurnstileProvider extends CaptchaProvider
->label(trans('admin/setting.captcha.info_label'))
->columnSpan(2)
->content(new HtmlString(trans('admin/setting.captcha.info'))),
]);
}
public function getIcon(): string
public function getIcon(): ?string
{
return 'tabler-brand-cloudflare';
}
public static function register(Application $app): self
{
return new self($app);
}
/**
* @return array<string, string|bool>
* @throws Exception
*/
public function validateResponse(?string $captchaResponse = null): array
public function validateResponse(?string $captchaResponse = null): void
{
$captchaResponse ??= request()->get('cf-turnstile-response');
@@ -84,22 +83,33 @@ class TurnstileProvider extends CaptchaProvider
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret,
'response' => $captchaResponse,
]);
])
->json();
return count($response->json()) ? $response->json() : [
'success' => false,
'message' => 'Unknown error occurred, please try again',
];
if (!$response['success']) {
match ($response['error-codes'][0] ?? null) {
'missing-input-secret' => throw new Exception('The secret parameter was not passed.'),
'invalid-input-secret' => throw new Exception('The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.'),
'missing-input-response' => throw new Exception('The response parameter (token) was not passed.'),
'invalid-input-response' => throw new Exception('The response parameter (token) is invalid or has expired.'),
'bad-request' => throw new Exception('The request was rejected because it was malformed.'),
'timeout-or-duplicate' => throw new Exception('The response parameter (token) has already been validated before.'),
default => throw new Exception('An internal error happened while validating the response.'),
};
}
if (!$this->verifyDomain($response['hostname'] ?? '')) {
throw new Exception('Domain verification failed.');
}
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
private function verifyDomain(string $hostname): bool
{
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true;
}
$requestUrl ??= request()->url;
$requestUrl = parse_url($requestUrl);
$requestUrl = parse_url(request()->url());
return $hostname === array_get($requestUrl, 'host');
}

View File

@@ -1,51 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
abstract class FeatureProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @param string[] $id
* @return self|static[]
*/
public static function getProviders(string|array|null $id = null): array|self
{
if (is_array($id)) {
return array_intersect_key(static::$providers, array_flip($id));
}
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'");
}
return;
}
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
/**
* A matching subset string (case-insensitive) from the console output
*
* @return array<string>
*/
abstract public function getListeners(): array;
abstract public function getAction(): Action;
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
interface FeatureSchemaInterface
{
/** @return string[] */
public function getListeners(): array;
public function getId(): string;
public function getAction(): Action;
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Extensions\Features;
class FeatureService
{
/** @var FeatureSchemaInterface[] */
private array $schemas = [];
/**
* @return FeatureSchemaInterface[]
*/
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?FeatureSchemaInterface
{
return array_get($this->schemas, $id);
}
/**
* @param ?string[] $features
* @return FeatureSchemaInterface[]
*/
public function getActiveSchemas(?array $features = []): array
{
return collect($this->schemas)->only($features)->all();
}
public function register(FeatureSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/**
* @param ?string[] $features
* @return array<string, array<string>>
*/
public function getMappings(?array $features = []): array
{
return collect($this->getActiveSchemas($features))
->mapWithKeys(fn (FeatureSchemaInterface $schema) => [
$schema->getId() => $schema->getListeners(),
])->all();
}
}

View File

@@ -1,7 +1,8 @@
<?php
namespace App\Extensions\Features;
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
@@ -15,18 +16,12 @@ use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
class GSLToken extends FeatureProvider
class GSLTokenSchema implements FeatureSchemaInterface
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
@@ -119,9 +114,4 @@ class GSLToken extends FeatureProvider
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,7 +1,8 @@
<?php
namespace App\Extensions\Features;
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
@@ -12,15 +13,9 @@ use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
class JavaVersion extends FeatureProvider
class JavaVersionSchema implements FeatureSchemaInterface
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
@@ -92,9 +87,4 @@ class JavaVersion extends FeatureProvider
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,7 +1,8 @@
<?php
namespace App\Extensions\Features;
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Repositories\Daemon\DaemonPowerRepository;
@@ -9,17 +10,11 @@ use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class MinecraftEula extends FeatureProvider
class MinecraftEulaSchema implements FeatureSchemaInterface
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
@@ -63,9 +58,4 @@ class MinecraftEula extends FeatureProvider
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,19 +1,14 @@
<?php
namespace App\Extensions\Features;
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class PIDLimit extends FeatureProvider
class PIDLimitSchema implements FeatureSchemaInterface
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
@@ -68,9 +63,4 @@ class PIDLimit extends FeatureProvider
->modalCancelActionLabel('Close')
->action(fn () => null);
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,19 +1,14 @@
<?php
namespace App\Extensions\Features;
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class SteamDiskSpace extends FeatureProvider
class SteamDiskSpaceSchema implements FeatureSchemaInterface
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
@@ -56,9 +51,4 @@ class SteamDiskSpace extends FeatureProvider
->modalCancelActionLabel('Close')
->action(fn () => null);
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Extensions\OAuth;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Wizard\Step;
interface OAuthSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function getConfigKey(): string;
/** @return ?class-string */
public function getSocialiteProvider(): ?string;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getServiceConfig(): array;
/** @return Component[] */
public function getSettingsForm(): array;
/** @return Step[] */
public function getSetupSteps(): array;
public function getIcon(): ?string;
public function getHexColor(): ?string;
public function isEnabled(): bool;
public function shouldCreateMissingUsers(): bool;
public function shouldLinkMissingUsers(): bool;
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Extensions\OAuth;
use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;
class OAuthService
{
/** @var OAuthSchemaInterface[] */
private array $schemas = [];
/** @return OAuthSchemaInterface[] */
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?OAuthSchemaInterface
{
return array_get($this->schemas, $id);
}
/** @return OAuthSchemaInterface[] */
public function getEnabled(): array
{
return collect($this->schemas)
->filter(fn (OAuthSchemaInterface $schema) => $schema->isEnabled())
->all();
}
public function register(OAuthSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
config()->set('services.' . $schema->getId(), array_merge($schema->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $schema->getId()]));
if ($schema->getSocialiteProvider()) {
Event::listen(fn (SocialiteWasCalled $event) => $event->extendSocialite($schema->getId(), $schema->getSocialiteProvider()));
}
$this->schemas[$schema->getId()] = $schema;
}
}

View File

@@ -1,38 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Illuminate\Foundation\Application;
final class CommonProvider extends OAuthProvider
{
protected function __construct(protected Application $app, private string $id, private ?string $providerClass, private ?string $icon, private ?string $hexColor)
{
parent::__construct($app);
}
public function getId(): string
{
return $this->id;
}
public function getProviderClass(): ?string
{
return $this->providerClass;
}
public function getIcon(): ?string
{
return $this->icon;
}
public function getHexColor(): ?string
{
return $this->hexColor;
}
public static function register(Application $app, string $id, ?string $providerClass = null, ?string $icon = null, ?string $hexColor = null): static
{
return new self($app, $id, $providerClass, $icon, $hexColor);
}
}

View File

@@ -1,25 +1,19 @@
<?php
namespace App\Extensions\OAuth\Providers;
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\TextInput;
use Illuminate\Foundation\Application;
use SocialiteProviders\Authentik\Provider;
final class AuthentikProvider extends OAuthProvider
final class AuthentikSchema extends OAuthSchema
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'authentik';
}
public function getProviderClass(): string
public function getSocialiteProvider(): string
{
return Provider::class;
}
@@ -66,9 +60,4 @@ final class AuthentikProvider extends OAuthProvider
{
return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d');
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Extensions\OAuth\Schemas;
final class CommonSchema extends OAuthSchema
{
public function __construct(
private readonly string $id,
private readonly ?string $name = null,
private readonly ?string $configName = null,
private readonly ?string $icon = null,
private readonly ?string $hexColor = null,
) {}
public function getId(): string
{
return $this->id;
}
public function getName(): string
{
return $this->name ?? parent::getName();
}
public function getConfigKey(): string
{
return $this->configName ?? parent::getConfigKey();
}
public function getIcon(): ?string
{
return $this->icon;
}
public function getHexColor(): ?string
{
return $this->hexColor;
}
}

View File

@@ -1,29 +1,23 @@
<?php
namespace App\Extensions\OAuth\Providers;
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Discord\Provider;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class DiscordProvider extends OAuthProvider
final class DiscordSchema extends OAuthSchema
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'discord';
}
public function getProviderClass(): string
public function getSocialiteProvider(): string
{
return Provider::class;
}
@@ -56,9 +50,4 @@ final class DiscordProvider extends OAuthProvider
{
return '#5865F2';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,22 +1,16 @@
<?php
namespace App\Extensions\OAuth\Providers;
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GithubProvider extends OAuthProvider
final class GithubSchema extends OAuthSchema
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'github';
@@ -55,9 +49,4 @@ final class GithubProvider extends OAuthProvider
{
return '#4078c0';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,22 +1,16 @@
<?php
namespace App\Extensions\OAuth\Providers;
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GitlabProvider extends OAuthProvider
final class GitlabSchema extends OAuthSchema
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'gitlab';
@@ -68,9 +62,4 @@ final class GitlabProvider extends OAuthProvider
{
return '#fca326';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -1,61 +1,24 @@
<?php
namespace App\Extensions\OAuth\Providers;
namespace App\Extensions\OAuth\Schemas;
use App\Extensions\OAuth\OAuthSchemaInterface;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Event;
use Filament\Forms\Set;
use Illuminate\Support\Str;
use SocialiteProviders\Manager\SocialiteWasCalled;
abstract class OAuthProvider
abstract class OAuthSchema implements OAuthSchemaInterface
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @return self|static[]
*/
public static function get(?string $id = null): array|self
{
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate OAuth provider with id '{$this->getId()}'");
}
return;
}
config()->set('services.' . $this->getId(), array_merge($this->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $this->getId()]));
if ($this->getProviderClass()) {
Event::listen(function (SocialiteWasCalled $event) {
$event->extendSocialite($this->getId(), $this->getProviderClass());
});
}
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
public function getProviderClass(): ?string
public function getSocialiteProvider(): ?string
{
return null;
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getServiceConfig(): array
{
$id = Str::upper($this->getId());
@@ -92,6 +55,28 @@ abstract class OAuthProvider
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_SECRET")),
Toggle::make("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")
->label(trans('admin/setting.oauth.create_missing_users'))
->columnSpanFull()
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")),
Toggle::make("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")
->label(trans('admin/setting.oauth.link_missing_users'))
->columnSpanFull()
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")),
];
}
@@ -112,6 +97,13 @@ abstract class OAuthProvider
return Str::title($this->getId());
}
public function getConfigKey(): string
{
$id = Str::upper($this->getId());
return "OAUTH_{$id}_ENABLED";
}
public function getIcon(): ?string
{
return null;
@@ -128,4 +120,18 @@ abstract class OAuthProvider
return env("OAUTH_{$id}_ENABLED", false);
}
public function shouldCreateMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
}
}

View File

@@ -1,28 +1,22 @@
<?php
namespace App\Extensions\OAuth\Providers;
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Steam\Provider;
final class SteamProvider extends OAuthProvider
final class SteamSchema extends OAuthSchema
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'steam';
}
public function getProviderClass(): string
public function getSocialiteProvider(): string
{
return Provider::class;
}
@@ -73,9 +67,4 @@ final class SteamProvider extends OAuthProvider
{
return '#00adee';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@@ -123,7 +123,7 @@ class Health extends Page
return $carry;
}, []);
return trans('admin/health.checks.failed') . implode(', ', $failedNames);
return trans('admin/health.checks.failed', ['checks' => implode(', ', $failedNames)]);
}
public static function getNavigationIcon(): string

View File

@@ -2,14 +2,17 @@
namespace App\Filament\Admin\Pages;
use App\Extensions\Avatar\AvatarProvider;
use App\Extensions\Captcha\Providers\CaptchaProvider;
use App\Extensions\OAuth\Providers\OAuthProvider;
use App\Extensions\Avatar\AvatarService;
use App\Extensions\Captcha\CaptchaService;
use App\Extensions\OAuth\OAuthService;
use App\Models\Backup;
use App\Notifications\MailTested;
use App\Traits\EnvironmentWriterTrait;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Component;
@@ -44,14 +47,23 @@ use Illuminate\Support\Str;
*/
class Settings extends Page implements HasForms
{
use CanCustomizeHeaderActions, InteractsWithHeaderActions {
CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions;
}
use CanCustomizeHeaderWidgets;
use EnvironmentWriterTrait;
use InteractsWithForms;
use InteractsWithHeaderActions;
protected static ?string $navigationIcon = 'tabler-settings';
protected static string $view = 'filament.pages.settings';
protected OAuthService $oauthService;
protected AvatarService $avatarService;
protected CaptchaService $captchaService;
/** @var array<mixed>|null */
public ?array $data = [];
@@ -60,6 +72,13 @@ class Settings extends Page implements HasForms
$this->form->fill();
}
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService): void
{
$this->oauthService = $oauthService;
$this->avatarService = $avatarService;
$this->captchaService = $captchaService;
}
public static function canAccess(): bool
{
return auth()->user()->can('view settings');
@@ -100,7 +119,7 @@ class Settings extends Page implements HasForms
->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('OAuth')
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth')
->schema($this->oauthSettings()),
@@ -150,16 +169,6 @@ class Settings extends Page implements HasForms
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
->default(env('APP_DEBUG', config('app.debug'))),
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
->label(trans('admin/setting.general.navigation'))
->inline()
->options([
false => trans('admin/setting.general.sidebar'),
true => trans('admin/setting.general.topbar'),
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
]),
Group::make()
->columns(2)
@@ -167,7 +176,7 @@ class Settings extends Page implements HasForms
Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider'))
->native(false)
->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()]))
->options($this->avatarService->getMappings())
->selectablePlaceholder(false)
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
@@ -258,15 +267,14 @@ class Settings extends Page implements HasForms
{
$formFields = [];
$captchaProviders = CaptchaProvider::get();
foreach ($captchaProviders as $captchaProvider) {
$id = Str::upper($captchaProvider->getId());
$name = Str::title($captchaProvider->getId());
$captchaSchemas = $this->captchaService->getAll();
foreach ($captchaSchemas as $schema) {
$id = Str::upper($schema->getId());
$formFields[] = Section::make($name)
$formFields[] = Section::make($schema->getName())
->columns(5)
->icon($captchaProvider->getIcon() ?? 'tabler-shield')
->collapsed(fn () => !env("CAPTCHA_{$id}_ENABLED", false))
->icon($schema->getIcon() ?? 'tabler-shield')
->collapsed(fn () => !$schema->isEnabled())
->collapsible()
->schema([
Hidden::make("CAPTCHA_{$id}_ENABLED")
@@ -277,21 +285,14 @@ class Settings extends Page implements HasForms
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
->label(trans('admin/setting.captcha.disable'))
->color('danger')
->action(function (Set $set) use ($id) {
$set("CAPTCHA_{$id}_ENABLED", false);
}),
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", false)),
FormAction::make("enable_captcha_$id")
->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED"))
->label(trans('admin/setting.captcha.enable'))
->color('success')
->action(function (Set $set) use ($id, $captchaProviders) {
foreach ($captchaProviders as $captchaProvider) {
$loopId = Str::upper($captchaProvider->getId());
$set("CAPTCHA_{$loopId}_ENABLED", $loopId === $id);
}
}),
->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", true)),
])->columnSpan(1),
Group::make($captchaProvider->getSettingsForm())
Group::make($schema->getSettingsForm())
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
->columns(4)
->columnSpan(4),
@@ -527,39 +528,37 @@ class Settings extends Page implements HasForms
{
$formFields = [];
$oauthProviders = OAuthProvider::get();
foreach ($oauthProviders as $oauthProvider) {
$id = Str::upper($oauthProvider->getId());
$name = Str::title($oauthProvider->getId());
$oauthSchemas = $this->oauthService->getAll();
foreach ($oauthSchemas as $schema) {
$id = Str::upper($schema->getId());
$key = $schema->getConfigKey();
$formFields[] = Section::make($name)
$formFields[] = Section::make($schema->getName())
->columns(5)
->icon($oauthProvider->getIcon() ?? 'tabler-brand-oauth')
->collapsed(fn () => !env("OAUTH_{$id}_ENABLED", false))
->icon($schema->getIcon() ?? 'tabler-brand-oauth')
->collapsed(fn () => !env($key, false))
->collapsible()
->schema([
Hidden::make("OAUTH_{$id}_ENABLED")
Hidden::make($key)
->live()
->default(env("OAUTH_{$id}_ENABLED")),
->default(env($key)),
Actions::make([
FormAction::make("disable_oauth_$id")
->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED"))
->visible(fn (Get $get) => $get($key))
->label(trans('admin/setting.oauth.disable'))
->color('danger')
->action(function (Set $set) use ($id) {
$set("OAUTH_{$id}_ENABLED", false);
}),
->action(fn (Set $set) => $set($key, false)),
FormAction::make("enable_oauth_$id")
->visible(fn (Get $get) => !$get("OAUTH_{$id}_ENABLED"))
->visible(fn (Get $get) => !$get($key))
->label(trans('admin/setting.oauth.enable'))
->color('success')
->steps($oauthProvider->getSetupSteps())
->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $name)
->steps($schema->getSetupSteps())
->modalHeading(trans('admin/setting.oauth.enable_schema', ['schema' => $schema->getName()]))
->modalSubmitActionLabel(trans('admin/setting.oauth.enable'))
->modalCancelAction(false)
->action(function ($data, Set $set) use ($id) {
->action(function ($data, Set $set) use ($key) {
$data = array_merge([
"OAUTH_{$id}_ENABLED" => 'true',
$key => 'true',
], $data);
foreach ($data as $key => $value) {
@@ -567,8 +566,8 @@ class Settings extends Page implements HasForms
}
}),
])->columnSpan(1),
Group::make($oauthProvider->getSettingsForm())
->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED"))
Group::make($schema->getSettingsForm())
->visible(fn (Get $get) => $get($key))
->columns(4)
->columnSpan(4),
]);
@@ -791,7 +790,8 @@ class Settings extends Page implements HasForms
}
}
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
Action::make('save')

View File

@@ -6,11 +6,16 @@ use App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteAction;
@@ -20,6 +25,11 @@ use Illuminate\Database\Eloquent\Builder;
class ApiKeyResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = ApiKey::class;
protected static ?string $navigationIcon = 'tabler-key';
@@ -56,7 +66,7 @@ class ApiKeyResource extends Resource
return trans('admin/dashboard.advanced');
}
public static function table(Table $table): Table
public static function defaultTable(Table $table): Table
{
return $table
->columns([
@@ -86,13 +96,13 @@ class ApiKeyResource extends Resource
])
->emptyStateIcon('tabler-key')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/apikey.empty_table'))
->emptyStateHeading(trans('admin/apikey.empty'))
->emptyStateActions([
CreateAction::make(),
]);
}
public static function form(Form $form): Form
public static function defaultForm(Form $form): Form
{
return $form
->schema([
@@ -142,7 +152,8 @@ class ApiKeyResource extends Resource
]);
}
public static function getPages(): array
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListApiKeys::route('/'),

View File

@@ -4,16 +4,24 @@ namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Admin\Resources\ApiKeyResource;
use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateApiKey extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ApiKeyResource::class;
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),

View File

@@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\ApiKeyResource\Pages;
use App\Filament\Admin\Resources\ApiKeyResource;
use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListApiKeys extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ApiKeyResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()

View File

@@ -3,12 +3,19 @@
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers;
use App\Models\DatabaseHost;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
@@ -20,6 +27,11 @@ use Illuminate\Database\Eloquent\Builder;
class DatabaseHostResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = DatabaseHost::class;
protected static ?string $navigationIcon = 'tabler-database';
@@ -51,7 +63,7 @@ class DatabaseHostResource extends Resource
return trans('admin/dashboard.advanced');
}
public static function table(Table $table): Table
public static function defaultTable(Table $table): Table
{
return $table
->columns([
@@ -89,7 +101,7 @@ class DatabaseHostResource extends Resource
]);
}
public static function form(Form $form): Form
public static function defaultForm(Form $form): Form
{
return $form
->schema([
@@ -150,7 +162,16 @@ class DatabaseHostResource extends Resource
]);
}
public static function getPages(): array
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
RelationManagers\DatabasesRelationManager::class,
];
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListDatabaseHosts::route('/'),

View File

@@ -4,6 +4,8 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Placeholder;
@@ -26,6 +28,8 @@ use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class CreateDatabaseHost extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use HasWizard;
protected static string $resource = DatabaseHostResource::class;

View File

@@ -3,9 +3,12 @@
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
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 App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
@@ -15,6 +18,9 @@ use PDOException;
class EditDatabaseHost extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseHostResource::class;
private HostUpdateService $hostUpdateService;
@@ -24,7 +30,8 @@ class EditDatabaseHost extends EditRecord
$this->hostUpdateService = $hostUpdateService;
}
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make()
@@ -39,17 +46,6 @@ class EditDatabaseHost extends EditRecord
return [];
}
public function getRelationManagers(): array
{
if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) {
return [
DatabasesRelationManager::class,
];
}
return [];
}
protected function handleRecordUpdate(Model $record, array $data): Model
{
if (!$record instanceof DatabaseHost) {

View File

@@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Models\DatabaseHost;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListDatabaseHosts extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseHostResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()

View File

@@ -3,29 +3,25 @@
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource;
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewDatabaseHost extends ViewRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = DatabaseHostResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),
];
}
public function getRelationManagers(): array
{
if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) {
return [
DatabasesRelationManager::class,
];
}
return [];
}
}

View File

@@ -3,11 +3,19 @@
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\EggResource\Pages;
use App\Filament\Admin\Resources\EggResource\RelationManagers;
use App\Models\Egg;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
class EggResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
protected static ?string $model = Egg::class;
protected static ?string $navigationIcon = 'tabler-eggs';
@@ -21,7 +29,7 @@ class EggResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
}
public static function getNavigationLabel(): string
@@ -44,7 +52,16 @@ class EggResource extends Resource
return ['name', 'tags', 'uuid', 'id'];
}
public static function getPages(): array
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
RelationManagers\ServersRelationManager::class,
];
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListEggs::route('/'),

View File

@@ -6,6 +6,10 @@ use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Admin\Resources\EggResource;
use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Hidden;
@@ -28,11 +32,15 @@ use Illuminate\Validation\Rules\Unique;
class CreateEgg extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = EggResource::class;
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),
@@ -49,7 +57,8 @@ class CreateEgg extends CreateRecord
return $form
->schema([
Tabs::make()->tabs([
Tab::make(trans('admin/egg.tabs.configuration'))
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
TextInput::make('name')
@@ -115,7 +124,8 @@ class CreateEgg extends CreateRecord
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make(trans('admin/egg.tabs.process_management'))
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
@@ -138,7 +148,8 @@ class CreateEgg extends CreateRecord
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make(trans('admin/egg.tabs.egg_variables'))
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
@@ -199,7 +210,7 @@ class CreateEgg extends CreateRecord
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
@@ -231,7 +242,8 @@ class CreateEgg extends CreateRecord
]),
]),
]),
Tab::make(trans('admin/egg.tabs.install_script'))
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')
@@ -246,7 +258,11 @@ class CreateEgg extends CreateRecord
->native(false)
->selectablePlaceholder(false)
->default('bash')
->options(['bash', 'ash', '/bin/bash'])
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))

View File

@@ -4,12 +4,15 @@ namespace App\Filament\Admin\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Admin\Resources\EggResource;
use App\Filament\Admin\Resources\EggResource\RelationManagers\ServersRelationManager;
use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction;
use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Models\Egg;
use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
@@ -31,6 +34,9 @@ use Illuminate\Validation\Rules\Unique;
class EditEgg extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = EggResource::class;
public function form(Form $form): Form
@@ -38,7 +44,8 @@ class EditEgg extends EditRecord
return $form
->schema([
Tabs::make()->tabs([
Tab::make(trans('admin/egg.tabs.configuration'))
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->icon('tabler-egg')
->schema([
@@ -109,7 +116,8 @@ class EditEgg extends EditRecord
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make(trans('admin/egg.tabs.process_management'))
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon('tabler-server-cog')
->schema([
@@ -129,7 +137,8 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make(trans('admin/egg.tabs.egg_variables'))
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon('tabler-variable')
->schema([
@@ -190,7 +199,7 @@ class EditEgg extends EditRecord
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
@@ -222,7 +231,8 @@ class EditEgg extends EditRecord
]),
]),
]),
Tab::make(trans('admin/egg.tabs.install_script'))
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon('tabler-file-download')
->schema([
@@ -237,7 +247,11 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.script_entry'))
->native(false)
->selectablePlaceholder(false)
->options(['bash', 'ash', '/bin/bash'])
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))
@@ -251,7 +265,8 @@ class EditEgg extends EditRecord
]);
}
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make()
@@ -273,11 +288,4 @@ class EditEgg extends EditRecord
{
return [];
}
public function getRelationManagers(): array
{
return [
ServersRelationManager::class,
];
}
}

View File

@@ -10,6 +10,10 @@ use App\Filament\Components\Tables\Actions\UpdateEggAction;
use App\Filament\Components\Tables\Actions\UpdateEggBulkAction;
use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Egg;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction as CreateHeaderAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction;
@@ -23,6 +27,9 @@ use Illuminate\Support\Str;
class ListEggs extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = EggResource::class;
public function table(Table $table): Table
@@ -95,7 +102,8 @@ class ListEggs extends ListRecords
]);
}
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
ImportEggHeaderAction::make()

View File

@@ -22,7 +22,7 @@ class ServersRelationManager extends RelationManager
->heading(trans('admin/egg.servers'))
->columns([
TextColumn::make('user.username')
->label('Owner')
->label(trans('admin/server.owner'))
->icon('tabler-user')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->sortable(),
@@ -38,8 +38,9 @@ class ServersRelationManager extends RelationManager
->label(trans('admin/server.docker_image')),
SelectColumn::make('allocation.id')
->label(trans('admin/server.primary_allocation'))
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->disabled()
->options(fn (Server $server) => $server->allocations->take(1)->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
->placeholder('None')
->sortable(),
]);
}

View File

@@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\MountResource\Pages;
use App\Models\Mount;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
@@ -11,6 +15,7 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
@@ -22,6 +27,11 @@ use Illuminate\Database\Eloquent\Builder;
class MountResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = Mount::class;
protected static ?string $navigationIcon = 'tabler-layers-linked';
@@ -53,7 +63,7 @@ class MountResource extends Resource
return trans('admin/dashboard.advanced');
}
public static function table(Table $table): Table
public static function defaultTable(Table $table): Table
{
return $table
->columns([
@@ -94,7 +104,7 @@ class MountResource extends Resource
]);
}
public static function form(Form $form): Form
public static function defaultForm(Form $form): Form
{
return $form
->schema([
@@ -162,7 +172,8 @@ class MountResource extends Resource
]);
}
public static function getPages(): array
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListMounts::route('/'),

View File

@@ -3,17 +3,25 @@
namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class CreateMount extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class;
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),

View File

@@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditMount extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make(),

View File

@@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource;
use App\Models\Mount;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListMounts extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()

View File

@@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\MountResource\Pages;
use App\Filament\Admin\Resources\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewMount extends ViewRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = MountResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),

View File

@@ -5,11 +5,18 @@ namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Admin\Resources\NodeResource\RelationManagers;
use App\Models\Node;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class NodeResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
protected static ?string $model = Node::class;
protected static ?string $navigationIcon = 'tabler-server-2';
@@ -33,7 +40,8 @@ class NodeResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
}
public static function getNavigationBadge(): ?string
@@ -41,7 +49,8 @@ class NodeResource extends Resource
return (string) static::getEloquentQuery()->count() ?: null;
}
public static function getRelations(): array
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
RelationManagers\AllocationsRelationManager::class,
@@ -49,7 +58,8 @@ class NodeResource extends Resource
];
}
public static function getPages(): array
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListNodes::route('/'),

View File

@@ -4,6 +4,8 @@ namespace App\Filament\Admin\Resources\NodeResource\Pages;
use App\Filament\Admin\Resources\NodeResource;
use App\Models\Node;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Grid;
@@ -21,6 +23,9 @@ use Illuminate\Support\HtmlString;
class CreateNode extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = NodeResource::class;
protected static bool $canCreateAnother = false;
@@ -84,16 +89,14 @@ class CreateNode extends CreateRecord
return;
}
$validRecords = gethostbynamel($state);
if ($validRecords) {
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);
$set('ip', collect($validRecords)->first());
return;
$set('ip', $ip);
} else {
$set('dns', false);
}
$set('dns', false);
})
->maxLength(255),
@@ -401,7 +404,7 @@ class CreateNode extends CreateRecord
type="submit"
size="sm"
>
Create Node
{{ trans('admin/node.create') }}
</x-filament::button>
BLADE))),
]);

View File

@@ -8,6 +8,8 @@ use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions;
use Filament\Forms;
@@ -34,6 +36,9 @@ use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class EditNode extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = NodeResource::class;
private DaemonConfigurationRepository $daemonConfigurationRepository;
@@ -59,7 +64,7 @@ class EditNode extends EditRecord
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tab::make('')
Tab::make('overview')
->label(trans('admin/node.tabs.overview'))
->icon('tabler-chart-area-line-filled')
->columns([
@@ -75,7 +80,7 @@ class EditNode extends EditRecord
->schema([
Placeholder::make('')
->label(trans('admin/node.wings_version'))
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' (' . trans('admin/node.latest') . ': ' . $versionService->latestWingsVersion() . ')'),
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])),
Placeholder::make('')
->label(trans('admin/node.cpu_threads'))
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
@@ -103,7 +108,8 @@ class EditNode extends EditRecord
View::make('filament.components.node-storage-chart')
->columnSpanFull(),
]),
Tab::make(trans('admin/node.tabs.basic_settings'))
Tab::make('basic_settings')
->label(trans('admin/node.tabs.basic_settings'))
->icon('tabler-server')
->schema([
TextInput::make('fqdn')
@@ -149,16 +155,14 @@ class EditNode extends EditRecord
return;
}
$validRecords = gethostbynamel($state);
if ($validRecords) {
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);
$set('ip', collect($validRecords)->first());
return;
$set('ip', $ip);
} else {
$set('dns', false);
}
$set('dns', false);
})
->maxLength(255),
TextInput::make('ip')
@@ -254,7 +258,7 @@ class EditNode extends EditRecord
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Tab::make('adv')
Tab::make('advanced_settings')
->label(trans('admin/node.tabs.advanced_settings'))
->columns([
'default' => 1,
@@ -522,7 +526,7 @@ class EditNode extends EditRecord
->suffix('%'),
]),
]),
Tab::make('Config')
Tab::make('config_file')
->label(trans('admin/node.tabs.config_file'))
->icon('tabler-code')
->schema([
@@ -550,7 +554,7 @@ class EditNode extends EditRecord
->modalFooterActionsAlignment(Alignment::Center)
->form([
ToggleButtons::make('docker')
->label('Type')
->label(trans('admin/node.auto_label'))
->live()
->helperText(trans('admin/node.auto_question'))
->inline()
@@ -613,10 +617,10 @@ class EditNode extends EditRecord
$data['config'] = $node->getYamlConfiguration();
if (!is_ip($node->fqdn)) {
$validRecords = gethostbynamel($node->fqdn);
if ($validRecords) {
$ip = get_ip_from_hostname($node->fqdn);
if ($ip) {
$data['dns'] = true;
$data['ip'] = collect($validRecords)->first();
$data['ip'] = $ip;
} else {
$data['dns'] = false;
}
@@ -630,7 +634,8 @@ class EditNode extends EditRecord
return [];
}
protected function getHeaderActions(): array
/** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
Actions\DeleteAction::make()

View File

@@ -6,6 +6,8 @@ use App\Filament\Admin\Resources\NodeResource;
use App\Filament\Components\Tables\Columns\NodeHealthColumn;
use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Node;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction;
@@ -16,6 +18,9 @@ use Filament\Tables\Table;
class ListNodes extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = NodeResource::class;
public function table(Table $table): Table
@@ -73,7 +78,8 @@ class ListNodes extends ListRecords
]);
}
protected function getHeaderActions(): array
/** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
Actions\CreateAction::make()

View File

@@ -58,8 +58,20 @@ class AllocationsRelationManager extends RelationManager
TextInputColumn::make('ip_alias')
->searchable()
->label(trans('admin/node.table.alias')),
TextInputColumn::make('notes')
->label(trans('admin/node.table.allocation_notes'))
->placeholder(trans('admin/node.table.no_notes')),
SelectColumn::make('ip')
->options(fn (Allocation $allocation) => collect($this->getOwnerRecord()->ipAddresses())->merge([$allocation->ip])->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->options(function (Allocation $allocation) {
$ips = Allocation::where('port', $allocation->port)->pluck('ip');
return collect($this->getOwnerRecord()->ipAddresses())
->diff($ips)
->unshift($allocation->ip)
->unique()
->mapWithKeys(fn (string $ip) => [$ip => $ip])
->all();
})
->selectablePlaceholder(false)
->searchable()
->label(trans('admin/node.table.ip')),
@@ -81,8 +93,7 @@ class AllocationsRelationManager extends RelationManager
->label(trans('admin/node.table.alias'))
->inlineLabel()
->default(null)
->helperText(trans('admin/node.alias_help'))
->required(false),
->helperText(trans('admin/node.alias_help')),
TagsInput::make('allocation_ports')
->placeholder('27015, 27017-27019')
->label(trans('admin/node.ports'))

View File

@@ -43,8 +43,10 @@ class NodesRelationManager extends RelationManager
->sortable(),
SelectColumn::make('allocation.id')
->label(trans('admin/node.primary_allocation'))
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->disabled(fn (Server $server) => $server->allocations->count() <= 1)
->options(fn (Server $server) => $server->allocations->take(1)->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
->selectablePlaceholder(fn (SelectColumn $select) => !$select->isDisabled())
->placeholder('None')
->sortable(),
TextColumn::make('memory')->label(trans('admin/node.memory'))->icon('tabler-device-desktop-analytics'),
TextColumn::make('cpu')->label(trans('admin/node.cpu'))->icon('tabler-cpu'),

View File

@@ -5,7 +5,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number;
class NodeCpuChart extends ChartWidget
{
@@ -82,8 +81,8 @@ class NodeCpuChart extends ChartWidget
{
$data = array_slice(end($this->cpuHistory), -60);
$cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language);
$max = Number::format($this->threads * 100, locale: auth()->user()->language);
$cpu = format_number($data['cpu'], maxPrecision: 2);
$max = format_number($this->threads * 100);
return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]);
}

View File

@@ -5,7 +5,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets;
use App\Models\Node;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number;
class NodeMemoryChart extends ChartWidget
{
@@ -85,12 +84,12 @@ class NodeMemoryChart extends ChartWidget
$latestMemoryUsed = array_slice(end($this->memoryHistory), -60);
$used = config('panel.use_binary_prefix')
? Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB';
? format_number($latestMemoryUsed['memory'], maxPrecision: 2) .' GiB'
: format_number($latestMemoryUsed['memory'], maxPrecision: 2) . ' GB';
$total = config('panel.use_binary_prefix')
? Number::format($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
: Number::format($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
? format_number($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2) .' GiB'
: format_number($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2) . ' GB';
return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]);
}

View File

@@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\RoleResource\Pages;
use App\Models\Role;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Component;
@@ -14,6 +18,7 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
@@ -26,6 +31,11 @@ use Spatie\Permission\Contracts\Permission;
class RoleResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = Role::class;
protected static ?string $navigationIcon = 'tabler-users-group';
@@ -49,7 +59,7 @@ class RoleResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
return !empty(auth()->user()->getCustomization()['top_navigation']) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
}
public static function getNavigationBadge(): ?string
@@ -57,7 +67,7 @@ class RoleResource extends Resource
return static::getModel()::count() ?: null;
}
public static function table(Table $table): Table
public static function defaultTable(Table $table): Table
{
return $table
->columns([
@@ -97,7 +107,7 @@ class RoleResource extends Resource
]);
}
public static function form(Form $form): Form
public static function defaultForm(Form $form): Form
{
$permissionSections = [];
@@ -119,7 +129,6 @@ class RoleResource extends Resource
->required()
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
TextInput::make('guard_name')
->label('Guard Name')
->default(Role::DEFAULT_GUARD_NAME)
->nullable()
->hidden(),
@@ -147,6 +156,8 @@ class RoleResource extends Resource
*/
private static function makeSection(string $model, array $options): Section
{
$model = ucwords($model);
$icon = null;
if (class_exists('\App\Filament\Admin\Resources\\' . $model . 'Resource')) {
@@ -198,7 +209,8 @@ class RoleResource extends Resource
]);
}
public static function getPages(): array
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListRoles::route('/'),

View File

@@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource;
use App\Models\Role;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
@@ -14,13 +18,17 @@ use Spatie\Permission\Models\Permission;
*/
class CreateRole extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
public Collection $permissions;
protected static string $resource = RoleResource::class;
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),

View File

@@ -4,6 +4,10 @@ namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource;
use App\Models\Role;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Arr;
@@ -15,6 +19,9 @@ use Spatie\Permission\Models\Permission;
*/
class EditRole extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = RoleResource::class;
public Collection $permissions;
@@ -45,7 +52,8 @@ class EditRole extends EditRecord
$this->record->syncPermissions($permissionModels);
}
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make()

View File

@@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListRoles extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make(),

View File

@@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\RoleResource\Pages;
use App\Filament\Admin\Resources\RoleResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewRole extends ViewRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),

View File

@@ -3,15 +3,23 @@
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Admin\Resources\ServerResource\RelationManagers;
use App\Models\Mount;
use App\Models\Server;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Get;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class ServerResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
protected static ?string $model = Server::class;
protected static ?string $navigationIcon = 'tabler-brand-docker';
@@ -35,7 +43,7 @@ class ServerResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
}
public static function getNavigationBadge(): ?string
@@ -66,7 +74,16 @@ class ServerResource extends Resource
->columnSpanFull();
}
public static function getPages(): array
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
RelationManagers\AllocationsRelationManager::class,
];
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListServers::route('/'),

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Components\Forms\Fields\StartupVariable;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\Node;
@@ -11,11 +12,11 @@ use App\Services\Allocations\AssignmentService;
use App\Services\Servers\RandomWordService;
use App\Services\Servers\ServerCreationService;
use App\Services\Users\UserCreationService;
use Closure;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
@@ -39,12 +40,14 @@ use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
use LogicException;
class CreateServer extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ServerResource::class;
protected static bool $canCreateAnother = false;
@@ -123,12 +126,12 @@ class CreateServer extends CreateRecord
->live()
->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id')))
->searchable()
->required()
->preload()
->afterStateUpdated(function (Set $set, $state) {
$set('allocation_id', null);
$this->node = Node::find($state);
})
->required(),
}),
Select::make('owner_id')
->preload()
@@ -189,7 +192,7 @@ class CreateServer extends CreateRecord
$set('allocation_additional', null);
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
})
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address ?? '')
->placeholder(function (Get $get) {
$node = Node::find($get('node_id'));
@@ -243,9 +246,7 @@ class CreateServer extends CreateRecord
return collect(
$assignmentService->handle(Node::find($get('node_id')), $data)
)->first();
})
->required(),
}),
Repeater::make('allocation_additional')
->label(trans('admin/server.additional_allocations'))
->columnSpan([
@@ -263,9 +264,9 @@ class CreateServer extends CreateRecord
->preload()
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->prefixIcon('tabler-network')
->label('Additional Allocations')
->label(trans('admin/server.additional_allocations'))
->columnSpan(2)
->disabled(fn (Get $get) => $get('../../node_id') === null)
->disabled(fn (Get $get) => $get('../../allocation_id') === null || $get('../../node_id') === null)
->searchable(['ip', 'port', 'ip_alias'])
->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address)
->placeholder(trans('admin/server.select_additional'))
@@ -426,7 +427,7 @@ class CreateServer extends CreateRecord
),
Repeater::make('server_variables')
->label('')
->hiddenLabel()
->relationship('serverVariables', fn (Builder $query) => $query->orderByPowerJoins('variable.sort'))
->saveRelationshipsBeforeChildrenUsing(null)
->saveRelationshipsUsing(null)
@@ -436,49 +437,15 @@ class CreateServer extends CreateRecord
->deletable(false)
->default([])
->hidden(fn ($state) => empty($state))
->schema(function () {
$text = TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->required(fn (Get $get) => in_array('required', $get('rules')))
->rules(
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $get('rules'),
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'))->toString();
$fail($message);
}
},
);
$select = Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
$components = [$text, $select];
foreach ($components as &$component) {
$component = $component
->live(onBlur: true)
->hintIcon('tabler-code')
->label(fn (Get $get) => $get('name'))
->hintIconTooltip(fn (Get $get) => implode('|', $get('rules')))
->prefix(fn (Get $get) => '{{' . $get('env_variable') . '}}')
->helperText(fn (Get $get) => empty($get('description')) ? '—' : $get('description'))
->afterStateUpdated(function (Set $set, Get $get, $state) {
$environment = $get($envPath = '../../environment');
$environment[$get('env_variable')] = $state;
$set($envPath, $environment);
});
}
return $components;
})
->schema([
StartupVariable::make('variable_value')
->fromForm()
->afterStateUpdated(function (Set $set, Get $get, $state) {
$environment = $get($envPath = '../../environment');
$environment[$get('env_variable')] = $state;
$set($envPath, $environment);
}),
])
->columnSpan(2),
]),
]),
@@ -794,7 +761,7 @@ class CreateServer extends CreateRecord
KeyValue::make('docker_labels')
->live()
->label('Container Labels')
->label(trans('admin/server.container_labels'))
->keyLabel(trans('admin/server.title'))
->valueLabel(trans('admin/server.description'))
->columnSpanFull(),
@@ -810,7 +777,7 @@ class CreateServer extends CreateRecord
type="submit"
size="sm"
>
Create Server
{{ trans('admin/server.create') }}
</x-filament::button>
BLADE))),
]);
@@ -828,7 +795,9 @@ class CreateServer extends CreateRecord
protected function handleRecordCreation(array $data): Model
{
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
if ($allocation_additional = array_get($data, 'allocation_additional')) {
$data['allocation_additional'] = collect($allocation_additional)->filter()->all();
}
try {
return $this->serverCreationService->handle($data);
@@ -844,40 +813,6 @@ class CreateServer extends CreateRecord
}
}
private function shouldHideComponent(Get $get, Component $component): bool
{
$containsRuleIn = collect($get('rules'))->reduce(
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
);
if ($component instanceof Select) {
return $containsRuleIn;
}
if ($component instanceof TextInput) {
return !$containsRuleIn;
}
throw new Exception('Component type not supported: ' . $component::class);
}
/**
* @return array<array-key, string>
*/
private function getSelectOptionsFromRules(Get $get): array
{
$inRule = collect($get('rules'))->reduce(
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
);
return str($inRule)
->after('in:')
->explode(',')
->each(fn ($value) => str($value)->trim())
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
/**
* @param string[] $portEntries
* @return array<int>

View File

@@ -5,9 +5,9 @@ namespace App\Filament\Admin\Resources\ServerResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Enums\SuspendAction;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
use App\Filament\Components\Forms\Actions\PreviewStartupAction;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Forms\Fields\StartupVariable;
use App\Filament\Server\Pages\Console;
use App\Models\Allocation;
use App\Models\Database;
@@ -26,10 +26,10 @@ use App\Services\Servers\ServerDeletionService;
use App\Services\Servers\SuspensionService;
use App\Services\Servers\ToggleInstallService;
use App\Services\Servers\TransferServerService;
use Closure;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Actions as FormActions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Component;
@@ -55,13 +55,15 @@ use Filament\Support\Enums\Alignment;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
use LogicException;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class EditServer extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ServerResource::class;
private DaemonServerRepository $daemonServerRepository;
@@ -85,7 +87,8 @@ class EditServer extends EditRecord
])
->columnSpanFull()
->tabs([
Tab::make(trans('admin/server.tabs.information'))
Tab::make('information')
->label(trans('admin/server.tabs.information'))
->icon('tabler-info-circle')
->schema([
TextInput::make('name')
@@ -217,7 +220,8 @@ class EditServer extends EditRecord
])
->disabled(),
]),
Tab::make(trans('admin/server.tabs.environment_configuration'))
Tab::make('environment_configuration')
->label(trans('admin/server.tabs.environment_configuration'))
->icon('tabler-brand-docker')
->schema([
Fieldset::make(trans('admin/server.resource_limits'))
@@ -524,7 +528,8 @@ class EditServer extends EditRecord
->columnSpanFull(),
]),
]),
Tab::make(trans('admin/server.egg'))
Tab::make('egg')
->label(trans('admin/server.egg'))
->icon('tabler-egg')
->columns([
'default' => 1,
@@ -613,7 +618,7 @@ class EditServer extends EditRecord
}),
Repeater::make('server_variables')
->label('')
->hiddenLabel()
->relationship('serverVariables', function (Builder $query) {
/** @var Server $server */
$server = $this->getRecord();
@@ -630,62 +635,26 @@ class EditServer extends EditRecord
return $query->orderByPowerJoins('variable.sort');
})
->grid()
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
foreach ($data as $key => $value) {
if (!isset($data['variable_value'])) {
$data['variable_value'] = '';
}
}
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['variable_value'] ??= '';
return $data;
})
->reorderable(false)->addable(false)->deletable(false)
->schema(function () {
$text = TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->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(...))
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
$components = [$text, $select];
foreach ($components as &$component) {
$component = $component
->live(onBlur: true)
->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;
})
->schema([
StartupVariable::make('variable_value')
->fromRecord(),
])
->columnSpan(6),
]),
Tab::make(trans('admin/server.mounts'))
Tab::make('mounts')
->label(trans('admin/server.mounts'))
->icon('tabler-layers-linked')
->schema(fn (Get $get) => [
ServerResource::getMountCheckboxList($get),
]),
Tab::make(trans('admin/server.databases'))
Tab::make('databases')
->label(trans('admin/server.databases'))
->hidden(fn () => !auth()->user()->can('viewAny', Database::class))
->icon('tabler-database')
->columns(4)
@@ -815,7 +784,8 @@ class EditServer extends EditRecord
]),
])->alignCenter()->columnSpanFull(),
]),
Tab::make(trans('admin/server.actions'))
Tab::make('actions')
->label(trans('admin/server.actions'))
->icon('tabler-settings')
->schema([
Fieldset::make(trans('admin/server.actions'))
@@ -948,12 +918,12 @@ class EditServer extends EditRecord
$transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', []));
Notification::make()
->title('Transfer started')
->title(trans('admin/server.notifications.transfer_started'))
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Transfer failed')
->title(trans('admin/server.notifications.transfer_failed'))
->body($exception->getMessage())
->danger()
->send();
@@ -1016,24 +986,28 @@ class EditServer extends EditRecord
->options(fn (Server $server) => Node::whereNot('id', $server->node->id)->pluck('name', 'id')->all()),
Select::make('allocation_id')
->label(trans('admin/server.primary_allocation'))
->required()
->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id)
->required(fn (Server $server) => $server->allocation_id)
->prefixIcon('tabler-network')
->disabled(fn (Get $get) => !$get('node_id'))
->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address]))
->searchable(['ip', 'port', 'ip_alias'])
->placeholder(trans('admin/server.select_allocation')),
Select::make('allocation_additional')
->label(trans('admin/server.additional_allocations'))
->disabled(fn (Get $get, Server $server) => !$get('node_id') || $server->allocations->count() <= 1)
->multiple()
->minItems(fn (Select $select) => $select->getMaxItems())
->maxItems(fn (Select $select, Server $server) => $select->isDisabled() ? null : $server->allocations->count() - 1)
->prefixIcon('tabler-network')
->disabled(fn (Get $get) => !$get('node_id'))
->required(fn (Server $server) => $server->allocations->count() > 1)
->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address]))
->searchable(['ip', 'port', 'ip_alias'])
->placeholder(trans('admin/server.select_additional')),
];
}
protected function getHeaderActions(): array
/** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{
/** @var Server $server */
$server = $this->getRecord();
@@ -1135,41 +1109,4 @@ class EditServer extends EditRecord
{
return null;
}
public function getRelationManagers(): array
{
return [
AllocationsRelationManager::class,
];
}
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\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);
}
/**
* @return array<string, string>
*/
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();
}
}

View File

@@ -5,6 +5,8 @@ namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Server\Pages\Console;
use App\Filament\Admin\Resources\ServerResource;
use App\Models\Server;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\Action;
@@ -17,6 +19,9 @@ use Filament\Tables\Table;
class ListServers extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = ServerResource::class;
public function table(Table $table): Table
@@ -68,14 +73,17 @@ class ListServers extends ListRecords
->searchable(),
SelectColumn::make('allocation_id')
->label(trans('admin/server.primary_allocation'))
->hidden(!auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
->hidden(fn () => !auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
->disabled(fn (Server $server) => $server->allocations->count() <= 1)
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
->selectablePlaceholder(false)
->selectablePlaceholder(fn (Server $server) => $server->allocations->count() <= 1)
->placeholder('None')
->sortable(),
TextColumn::make('allocation_id_readonly')
->label(trans('admin/server.primary_allocation'))
->hidden(auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
->state(fn (Server $server) => $server->allocation->address),
->hidden(fn () => auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty)
->disabled(fn (Server $server) => $server->allocations->count() <= 1)
->state(fn (Server $server) => $server->allocation->address ?? 'None'),
TextColumn::make('image')->hidden(),
TextColumn::make('backups_count')
->counts('backups')
@@ -101,7 +109,8 @@ class ListServers extends ListRecords
]);
}
protected function getHeaderActions(): array
/** @return array<Actions\Action|Actions\ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
Actions\CreateAction::make()

View File

@@ -12,8 +12,6 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Support\Exceptions\Halt;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\AssociateAction;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DissociateAction;
@@ -22,7 +20,6 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
/**
* @method Server getOwnerRecord()
@@ -37,7 +34,6 @@ class AllocationsRelationManager extends RelationManager
->selectCurrentPageOnly()
->recordTitleAttribute('address')
->recordTitle(fn (Allocation $allocation) => $allocation->address)
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
->inverseRelationship('server')
->heading(trans('admin/server.allocations'))
->columns([
@@ -47,6 +43,9 @@ class AllocationsRelationManager extends RelationManager
->label(trans('admin/server.port')),
TextInputColumn::make('ip_alias')
->label(trans('admin/server.alias')),
TextInputColumn::make('notes')
->label(trans('admin/server.notes'))
->placeholder(trans('admin/server.no_notes')),
IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled',
@@ -56,17 +55,17 @@ class AllocationsRelationManager extends RelationManager
true => 'warning',
default => 'gray',
})
->tooltip(fn (Allocation $allocation) => trans('admin/server.' . ($allocation->id === $this->getOwnerRecord()->allocation_id ? 'already' : 'make') . '_primary'))
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
->label(trans('admin/server.primary')),
])
->actions([
Action::make('make-primary')
->label(trans('admin/server.make_primary'))
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
DissociateAction::make()
->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id),
->after(function (Allocation $allocation) {
$allocation->update(['notes' => null]);
$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
}),
])
->headerActions([
CreateAction::make()->label(trans('admin/server.create_allocation'))
@@ -84,8 +83,7 @@ class AllocationsRelationManager extends RelationManager
->label(trans('admin/server.alias'))
->inlineLabel()
->default(null)
->helperText(trans('admin/server.alias_helper'))
->required(false),
->helperText(trans('admin/server.alias_helper')),
TagsInput::make('allocation_ports')
->placeholder('27015, 27017-27019')
->label(trans('admin/server.ports'))
@@ -103,22 +101,14 @@ class AllocationsRelationManager extends RelationManager
->preloadRecordSelect()
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
->recordSelectSearchColumns(['ip', 'port'])
->label(trans('admin/server.add_allocation')),
->label(trans('admin/server.add_allocation'))
->after(fn (array $data) => !$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]])),
])
->groupedBulkActions([
DissociateBulkAction::make()
->before(function (DissociateBulkAction $action, Collection $records) {
$records = $records->filter(function ($allocation) {
/** @var Allocation $allocation */
return $allocation->id !== $this->getOwnerRecord()->allocation_id;
});
if ($records->isEmpty()) {
$action->failureNotificationTitle(trans('admin/server.notifications.dissociate_primary'))->failure();
throw new Halt();
}
return $records;
->after(function () {
Allocation::whereNull('server_id')->update(['notes' => null]);
$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
}),
]);
}

View File

@@ -6,10 +6,16 @@ use App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource\RelationManagers;
use App\Models\Role;
use App\Models\User;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
@@ -22,6 +28,11 @@ use Illuminate\Database\Eloquent\Builder;
class UserResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'tabler-users';
@@ -45,7 +56,7 @@ class UserResource extends Resource
public static function getNavigationGroup(): ?string
{
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.user');
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.user');
}
public static function getNavigationBadge(): ?string
@@ -53,7 +64,7 @@ class UserResource extends Resource
return static::getModel()::count() ?: null;
}
public static function table(Table $table): Table
public static function defaultTable(Table $table): Table
{
return $table
->columns([
@@ -69,7 +80,7 @@ class UserResource extends Resource
->label(trans('admin/user.email'))
->icon('tabler-mail'),
IconColumn::make('use_totp')
->label('2FA')
->label(trans('profile.tabs.2fa'))
->visibleFrom('lg')
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
->boolean(),
@@ -99,7 +110,7 @@ class UserResource extends Resource
]);
}
public static function form(Form $form): Form
public static function defaultForm(Form $form): Form
{
return $form
->columns(['default' => 1, 'lg' => 3])
@@ -146,14 +157,16 @@ class UserResource extends Resource
]);
}
public static function getRelations(): array
/** @return class-string<RelationManager>[] */
public static function getDefaultRelations(): array
{
return [
RelationManagers\ServersRelationManager::class,
];
}
public static function getPages(): array
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),

View File

@@ -5,11 +5,18 @@ namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Models\Role;
use App\Services\Users\UserCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateUser extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class;
protected static bool $canCreateAnother = false;
@@ -21,7 +28,8 @@ class CreateUser extends CreateRecord
$this->service = $service;
}
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
$this->getCreateFormAction()->formId('form'),

View File

@@ -5,12 +5,19 @@ namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Models\User;
use App\Services\Users\UserUpdateService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
class EditUser extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class;
private UserUpdateService $service;
@@ -20,7 +27,8 @@ class EditUser extends EditRecord
$this->service = $service;
}
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make()

View File

@@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make(),

View File

@@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewUser extends ViewRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),

View File

@@ -70,8 +70,9 @@ class ServersRelationManager extends RelationManager
->sortable(),
SelectColumn::make('allocation.id')
->label(trans('admin/server.primary_allocation'))
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
->selectablePlaceholder(false)
->disabled()
->options(fn (Server $server) => $server->allocations->take(1)->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
->placeholder('None')
->sortable(),
TextColumn::make('image')->hidden(),
TextColumn::make('databases_count')

View File

@@ -3,21 +3,50 @@
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource\Pages\EditWebhookConfiguration;
use App\Livewire\AlertBanner;
use App\Models\WebhookConfiguration;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Forms\Get;
use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteAction;
use Filament\Forms\Set;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ReplicateAction;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Livewire\Features\SupportEvents\HandlesEvents;
use App\Enums\WebhookType;
use Filament\Forms\Components\Component;
use Livewire\Component as Livewire;
class WebhookResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
use CanModifyForm;
use CanModifyTable;
use HandlesEvents;
protected static ?string $model = WebhookConfiguration::class;
protected static ?string $navigationIcon = 'tabler-webhook';
@@ -49,10 +78,16 @@ class WebhookResource extends Resource
return trans('admin/dashboard.advanced');
}
public static function table(Table $table): Table
public static function defaultTable(Table $table): Table
{
return $table
->columns([
IconColumn::make('type'),
TextColumn::make('endpoint')
->label(trans('admin/webhook.table.endpoint'))
->formatStateUsing(fn (string $state) => str($state)->after('://'))
->limit(60)
->wrap(),
TextColumn::make('description')
->label(trans('admin/webhook.table.description')),
TextColumn::make('endpoint')
@@ -60,9 +95,15 @@ class WebhookResource extends Resource
])
->actions([
ViewAction::make()
->hidden(fn ($record) => static::canEdit($record)),
->hidden(fn (WebhookConfiguration $record) => static::canEdit($record)),
EditAction::make(),
DeleteAction::make(),
ReplicateAction::make()
->iconButton()
->tooltip(trans('filament-actions::replicate.single.label'))
->modal(false)
->excludeAttributes(['created_at', 'updated_at'])
->beforeReplicaSaved(fn (WebhookConfiguration $replica) => $replica->description .= ' Copy ' . now()->format('Y-m-d H:i:s'))
->successRedirectUrl(fn (WebhookConfiguration $replica) => EditWebhookConfiguration::getUrl(['record' => $replica])),
])
->groupedBulkActions([
DeleteBulkAction::make(),
@@ -72,33 +113,228 @@ class WebhookResource extends Resource
->emptyStateHeading(trans('admin/webhook.no_webhooks'))
->emptyStateActions([
CreateAction::make(),
])
->persistFiltersInSession()
->filters([
SelectFilter::make('type')
->options(WebhookType::class)
->attribute('type'),
]);
}
public static function form(Form $form): Form
public static function defaultForm(Form $form): Form
{
return $form
->schema([
TextInput::make('endpoint')
->label(trans('admin/webhook.endpoint'))
->activeUrl()
->required(),
ToggleButtons::make('type')
->live()
->inline()
->options(WebhookType::class)
->default(WebhookType::Regular->value),
TextInput::make('description')
->label(trans('admin/webhook.description'))
->required(),
CheckboxList::make('events')
->lazy()
->options(fn () => WebhookConfiguration::filamentCheckboxList())
->searchable()
->bulkToggleable()
->columns(3)
TextInput::make('endpoint')
->label(trans('admin/webhook.endpoint'))
->required()
->columnSpanFull()
->gridDirection('row')
->required(),
->afterStateUpdated(fn (string $state, Set $set) => $set('type', str($state)->contains('discord.com') ? WebhookType::Discord->value : WebhookType::Regular->value)),
Section::make(trans('admin/webhook.regular'))
->hidden(fn (Get $get) => $get('type') === WebhookType::Discord->value)
->dehydratedWhenHidden()
->schema(fn () => self::getRegularFields())
->headerActions([
Action::make('reset_headers')
->label(trans('admin/webhook.reset_headers'))
->color('danger')
->icon('heroicon-o-trash')
->action(fn (Get $get, Set $set) => $set('headers', [
'X-Webhook-Event' => '{{event}}',
])),
])
->formBefore(),
Section::make(trans('admin/webhook.discord'))
->hidden(fn (Get $get) => $get('type') === WebhookType::Regular->value)
->dehydratedWhenHidden()
->afterStateUpdated(fn (Livewire $livewire) => $livewire->dispatch('refresh-widget'))
->schema(fn () => self::getDiscordFields())
->view('filament.components.webhooksection')
->aside()
->formBefore(),
Section::make(trans('admin/webhook.events'))
->schema([
CheckboxList::make('events')
->live()
->options(fn () => WebhookConfiguration::filamentCheckboxList())
->searchable()
->bulkToggleable()
->columns(3)
->columnSpanFull()
->required(),
]),
]);
}
public static function getPages(): array
/** @return Component[] */
private static function getRegularFields(): array
{
return [
KeyValue::make('headers')
->label(trans('admin/webhook.headers'))
->default(fn () => [
'X-Webhook-Event' => '{{event}}',
]),
];
}
/** @return Component[] */
private static function getDiscordFields(): array
{
return [
Section::make(trans('admin/webhook.discord_message.profile'))
->collapsible()
->schema([
TextInput::make('username')
->live(debounce: 500)
->label(trans('admin/webhook.discord_message.username')),
TextInput::make('avatar_url')
->live(debounce: 500)
->label(trans('admin/webhook.discord_message.avatar_url')),
]),
Section::make(trans('admin/webhook.discord_message.message'))
->collapsible()
->schema([
TextInput::make('content')
->label(trans('admin/webhook.discord_message.message'))
->live(debounce: 500)
->required(fn (Get $get) => empty($get('embeds'))),
TextInput::make('thread_name')
->label(trans('admin/webhook.discord_message.forum_thread')),
CheckboxList::make('flags')
->label(trans('admin/webhook.discord_embed.flags'))
->options([
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds'),
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications'),
])
->descriptions([
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds_text'),
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications_text'),
]),
CheckboxList::make('allowed_mentions')
->label(trans('admin/webhook.discord_embed.allowed_mentions'))
->options([
'roles' => trans('admin/webhook.discord_embed.roles'),
'users' => trans('admin/webhook.discord_embed.users'),
'everyone' => trans('admin/webhook.discord_embed.everyone'),
]),
]),
Repeater::make('embeds')
->live(debounce: 500)
->itemLabel(fn (array $state) => $state['title'])
->addActionLabel(trans('admin/webhook.discord_embed.add_embed'))
->required(fn (Get $get) => empty($get('content')))
->reorderable()
->collapsible()
->maxItems(10)
->schema([
Section::make(trans('admin/webhook.discord_embed.author'))
->collapsible()
->collapsed()
->schema([
TextInput::make('author.name')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.author'))
->required(fn (Get $get) => filled($get('author.url')) || filled($get('author.icon_url'))),
TextInput::make('author.url')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.author_url')),
TextInput::make('author.icon_url')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.author_icon_url')),
]),
Section::make(trans('admin/webhook.discord_embed.body'))
->collapsible()
->collapsed()
->schema([
TextInput::make('title')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.title'))
->required(fn (Get $get) => $get('description') === null),
Textarea::make('description')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.body'))
->required(fn (Get $get) => $get('title') === null),
ColorPicker::make('color')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.color'))
->hex(),
TextInput::make('url')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.url')),
]),
Section::make(trans('admin/webhook.discord_embed.images'))
->collapsible()
->collapsed()
->schema([
TextInput::make('image.url')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.image_url')),
TextInput::make('thumbnail.url')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.image_thumbnail')),
]),
Section::make(trans('admin/webhook.discord_embed.footer'))
->collapsible()
->collapsed()
->schema([
TextInput::make('footer.text')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.footer')),
Checkbox::make('has_timestamp')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.has_timestamp')),
TextInput::make('footer.icon_url')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.footer_icon_url')),
]),
Section::make(trans('admin/webhook.discord_embed.fields'))
->collapsible()->collapsed()
->schema([
Repeater::make('fields')
->reorderable()
->addActionLabel(trans('admin/webhook.discord_embed.add_field'))
->collapsible()
->schema([
TextInput::make('name')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.field_name'))
->required(),
Textarea::make('value')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.field_value'))
->rows(4)
->required(),
Checkbox::make('inline')
->live(debounce: 500)
->label(trans('admin/webhook.discord_embed.inline_field')),
]),
]),
]),
];
}
public static function sendHelpBanner(): void
{
AlertBanner::make('discord_webhook_help')
->title(trans('admin/webhook.help'))
->body(trans('admin/webhook.help_text'))
->icon('tabler-question-mark')
->info()
->send();
}
/** @return array<string, PageRegistration> */
public static function getDefaultPages(): array
{
return [
'index' => Pages\ListWebhookConfigurations::route('/'),

View File

@@ -3,17 +3,27 @@
namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use App\Enums\WebhookType;
class CreateWebhookConfiguration extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = WebhookResource::class;
protected static bool $canCreateAnother = false;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
$this->getCancelFormAction()->formId('form'),
$this->getCreateFormAction()->formId('form'),
];
}
@@ -22,4 +32,46 @@ class CreateWebhookConfiguration extends CreateRecord
{
return [];
}
protected function mutateFormDataBeforeCreate(array $data): array
{
if (($data['type'] ?? null) === WebhookType::Discord->value) {
$embeds = data_get($data, 'embeds', []);
foreach ($embeds as &$embed) {
$embed['color'] = hexdec(str_replace('#', '', data_get($embed, 'color')));
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
}
$flags = collect($data['flags'] ?? [])->reduce(fn ($carry, $bit) => $carry | $bit, 0);
$tmp = collect([
'username' => data_get($data, 'username'),
'avatar_url' => data_get($data, 'avatar_url'),
'content' => data_get($data, 'content'),
'image' => data_get($data, 'image'),
'thumbnail' => data_get($data, 'thumbnail'),
'embeds' => $embeds,
'thread_name' => data_get($data, 'thread_name'),
'flags' => $flags,
'allowed_mentions' => data_get($data, 'allowed_mentions', []),
])->filter(fn ($key) => !empty($key))->all();
unset($data['username'], $data['avatar_url'], $data['content'], $data['image'], $data['thumbnail'], $data['embeds'], $data['thread_name'], $data['flags'], $data['allowed_mentions']);
$data['payload'] = $tmp;
}
return $data;
}
protected function getRedirectUrl(): string
{
return EditWebhookConfiguration::getUrl(['record' => $this->getRecord()]);
}
public function mount(): void
{
parent::mount();
WebhookResource::sendHelpBanner();
}
}

View File

@@ -3,17 +3,33 @@
namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use App\Models\WebhookConfiguration;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use App\Enums\WebhookType;
class EditWebhookConfiguration extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = WebhookResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
DeleteAction::make(),
Action::make('test_now')
->label(trans('admin/webhook.test_now'))
->color('primary')
->disabled(fn (WebhookConfiguration $webhookConfiguration) => count($webhookConfiguration->events) === 0)
->action(fn (WebhookConfiguration $webhookConfiguration) => $webhookConfiguration->run())
->tooltip(trans('admin/webhook.test_now_help')),
$this->getSaveFormAction()->formId('form'),
];
}
@@ -22,4 +38,95 @@ class EditWebhookConfiguration extends EditRecord
{
return [];
}
protected function mutateFormDataBeforeSave(array $data): array
{
if (($data['type'] ?? null) === WebhookType::Discord->value) {
$embeds = data_get($data, 'embeds', []);
foreach ($embeds as &$embed) {
$embed['color'] = hexdec(str_replace('#', '', data_get($embed, 'color')));
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
}
$flags = collect($data['flags'] ?? [])->reduce(fn ($carry, $bit) => $carry | $bit, 0);
$tmp = collect([
'username' => data_get($data, 'username'),
'avatar_url' => data_get($data, 'avatar_url'),
'content' => data_get($data, 'content'),
'image' => data_get($data, 'image'),
'thumbnail' => data_get($data, 'thumbnail'),
'embeds' => $embeds,
'thread_name' => data_get($data, 'thread_name'),
'flags' => $flags,
'allowed_mentions' => data_get($data, 'allowed_mentions', []),
])->filter(fn ($key) => !empty($key))->all();
unset($data['username'], $data['avatar_url'], $data['content'], $data['image'], $data['thumbnail'], $data['embeds'], $data['thread_name'], $data['flags'], $data['allowed_mentions']);
$data['payload'] = $tmp;
}
if (($data['type'] ?? null) === WebhookType::Regular->value && isset($data['headers']) && is_array($data['headers'])) {
$newHeaders = [];
foreach ($data['headers'] as $key => $value) {
$newKey = str_replace(' ', '-', $key);
$newHeaders[$newKey] = $value;
}
$data['headers'] = $newHeaders;
}
return $data;
}
protected function mutateFormDataBeforeFill(array $data): array
{
if (($data['type'] ?? null) === WebhookType::Discord->value) {
$embeds = data_get($data, 'payload.embeds', []);
foreach ($embeds as &$embed) {
$embed['color'] = '#' . dechex(data_get($embed, 'color'));
$embed = collect($embed)->filter(fn ($key) => is_array($key) ? array_filter($key, fn ($arr_key) => !empty($arr_key)) : !empty($key))->all();
}
$flags = data_get($data, 'payload.flags');
$flags = collect(range(0, PHP_INT_SIZE * 8 - 1))
->filter(fn ($i) => ($flags & (1 << $i)) !== 0)
->map(fn ($i) => 1 << $i)
->values();
$tmp = collect([
'username' => data_get($data, 'payload.username'),
'avatar_url' => data_get($data, 'payload.avatar_url'),
'content' => data_get($data, 'payload.content'),
'image' => data_get($data, 'payload.image'),
'thumbnail' => data_get($data, 'payload.thumbnail'),
'embeds' => $embeds,
'thread_name' => data_get($data, 'payload.thread_name'),
'flags' => $flags,
'allowed_mentions' => data_get($data, 'payload.allowed_mentions'),
])->filter(fn ($key) => !empty($key))->all();
unset($data['payload'], $data['created_at'], $data['updated_at'], $data['deleted_at']);
$data = array_merge($data, $tmp);
}
if (($data['type'] ?? null) === WebhookType::Regular->value) {
$data['headers'] = $data['headers'] ?? [];
}
return $data;
}
protected function afterSave(): void
{
$this->dispatch('refresh-widget');
}
public function mount(int|string $record): void
{
parent::mount($record);
WebhookResource::sendHelpBanner();
}
}

View File

@@ -4,14 +4,22 @@ namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource;
use App\Models\WebhookConfiguration;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListWebhookConfigurations extends ListRecords
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = WebhookResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
CreateAction::make()

View File

@@ -3,14 +3,22 @@
namespace App\Filament\Admin\Resources\WebhookResource\Pages;
use App\Filament\Admin\Resources\WebhookResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewWebhookConfiguration extends ViewRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
protected static string $resource = WebhookResource::class;
protected function getHeaderActions(): array
/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
return [
EditAction::make(),

View File

@@ -2,15 +2,13 @@
namespace App\Filament\Admin\Widgets;
use Filament\Actions\CreateAction;
use Filament\Widgets\Widget;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
class CanaryWidget extends Widget
class CanaryWidget extends FormWidget
{
protected static string $view = 'filament.admin.widgets.canary-widget';
protected static bool $isLazy = false;
protected static ?int $sort = 1;
public static function canView(): bool
@@ -18,15 +16,28 @@ class CanaryWidget extends Widget
return config('app.version') === 'canary';
}
public function getViewData(): array
public function form(Form $form): Form
{
return [
'actions' => [
CreateAction::make()
->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/issues', true),
],
];
return $form
->schema([
Section::make(trans('admin/dashboard.sections.intro-developers.heading'))
->icon('tabler-code')
->iconColor('primary')
->collapsible()
->collapsed()
->persistCollapsed()
->schema([
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-developers.content')),
Placeholder::make('')
->content(trans('admin/dashboard.sections.intro-developers.extra_note')),
])
->headerActions([
Action::make('issues')
->label(trans('admin/dashboard.sections.intro-developers.button_issues'))
->icon('tabler-brand-github')
->url('https://github.com/pelican-dev/panel/issues', true),
]),
]);
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace App\Filament\Admin\Widgets;
use App\Models\WebhookConfiguration;
use Filament\Widgets\Widget;
use Illuminate\Support\Carbon;
class DiscordPreview extends Widget
{
protected static string $view = 'filament.admin.widgets.discord-preview';
/** @var array<string, string> */
protected $listeners = [
'refresh-widget' => '$refresh',
];
protected static bool $isDiscovered = false; // Without this its shown on every Admin Pages
protected int|string|array $columnSpan = 1;
public ?WebhookConfiguration $record = null;
/** @var string|array<string, mixed>|null */
public string|array|null $payload = null;
/**
* @return array{
* link: callable,
* content: mixed,
* sender: array{name: string, avatar: string},
* embeds: array<int, mixed>,
* getTime: mixed
* }
*/
public function getViewData(): array
{
if (!$this->record || !$this->record->payload) {
return [
'link' => fn ($href, $child) => $href ? "<a href=\"$href\" target=\"_blank\" class=\"link\">$child</a>" : $child,
'content' => null,
'sender' => [
'name' => 'Pelican',
'avatar' => 'https://raw.githubusercontent.com/pelican-dev/panel/refs/heads/main/public/pelican.ico',
],
'embeds' => [],
'getTime' => 'Today at ' . Carbon::now()->format('h:i A'),
];
}
$data = $this->getWebhookSampleData();
if (is_string($this->record->payload)) {
$payload = $this->replaceVarsInStringPayload($this->record->payload, $data);
} else {
$payload = $this->replaceVarsInArrayPayload($this->record->payload, $data);
}
$embeds = data_get($payload, 'embeds', []);
foreach ($embeds as &$embed) {
if (data_get($embed, 'has_timestamp')) {
unset($embed['has_timestamp']);
$embed['timestamp'] = 'Today at ' . Carbon::now()->format('h:i A');
}
}
return [
'link' => fn ($href, $child) => $href ? sprintf('<a href="%s" target="_blank" class="link">%s</a>', $href, $child) : $child,
'content' => data_get($payload, 'content'),
'sender' => [
'name' => data_get($payload, 'username', 'Pelican'),
'avatar' => data_get($payload, 'avatar_url', 'https://raw.githubusercontent.com/pelican-dev/panel/refs/heads/main/public/pelican.ico'),
],
'embeds' => $embeds,
'getTime' => 'Today at ' . Carbon::now()->format('h:i A'),
];
}
/**
* @param array<string, mixed> $data
*/
private function replaceVarsInStringPayload(?string $payload, array $data): ?string
{
if ($payload === null) {
return null;
}
return preg_replace_callback('/{{\s*([\w\.]+)\s*}}/', fn ($m) => data_get($data, $m[1], $m[0]),
$payload
);
}
/**
* @param array<string, mixed>|null $payload
* @param array<string, mixed> $data
* @return array<string, mixed>|null
*/
private function replaceVarsInArrayPayload(?array $payload, array $data): ?array
{
if ($payload === null) {
return null;
}
foreach ($payload as $key => $value) {
if (is_string($value)) {
$payload[$key] = $this->replaceVarsInStringPayload($value, $data);
} elseif (is_array($value)) {
$payload[$key] = $this->replaceVarsInArrayPayload($value, $data);
}
}
return $payload;
}
/**
* @return array<string, mixed>
*/
public function getWebhookSampleData(): array
{
return [
'event' => 'updated: server',
'id' => 2,
'external_id' => 10,
'uuid' => '651fgbc1-dee6-4250-814e-10slda13f1e',
'uuid_short' => '651fgbc1',
'node_id' => 1,
'name' => 'Example Server',
'description' => 'This is an example server description.',
'status' => 'running',
'skip_scripts' => false,
'owner_id' => 1,
'memory' => 512,
'swap' => 128,
'disk' => 10240,
'io' => 500,
'cpu' => 500,
'threads' => '1, 3, 5',
'oom_killer' => false,
'allocation_id' => 4,
'egg_id' => 2,
'startup' => 'This is a example startup command.',
'image' => 'Image here',
'allocation_limit' => 5,
'database_limit' => 1,
'backup_limit' => 3,
'created_at' => '2025-03-17T15:20:32.000000Z',
'updated_at' => '2025-05-12T17:53:12.000000Z',
'installed_at' => '2025-04-27T21:06:01.000000Z',
'docker_labels' => [],
'allocation' => [
'id' => 4,
'node_id' => 1,
'ip' => '192.168.0.3',
'ip_alias' => null,
'port' => 25567,
'server_id' => 2,
'notes' => null,
'created_at' => '2025-03-17T15:20:09.000000Z',
'updated_at' => '2025-03-17T15:20:32.000000Z',
],
];
}
}

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