Compare commits

...

44 Commits

Author SHA1 Message Date
Boy132
5c3b0919aa Fix allocations by admins aren't locked by default (#1879) 2025-11-09 18:29:46 +01:00
Charles
f4ee33fa4f Hide new allocation action if server has 0 allocations. (#1878) 2025-11-09 12:11:14 -05:00
Charles
d8368c4cec Do no use stock notifications on actions (#1877) 2025-11-09 12:08:25 -05:00
Charles
aa35d7d001 Fix creating mounts (#1876) 2025-11-09 11:14:44 -05:00
JoanFo
3c25b43b46 Repair webhooks once again (#1815)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-11-09 09:35:00 -05:00
Charles
0891db5342 Reimplement Drag & Drop for file uploading 🎉 (#1858) 2025-11-09 09:24:12 -05:00
exefer
172436e012 Fix typo in failed upload message (#1874) 2025-11-09 12:58:56 +00:00
Charles
2b5403a4da Replace current panel log viewer with new and improved log viewer (#1834) 2025-11-08 19:31:51 -05:00
Charles
a30c45fbbe Add session key to use last used node, instead of latest created node (#1869)
Co-authored-by: Lance Pioch <git@lance.sh>
2025-11-08 17:09:41 -05:00
Copilot
b06df23823 Add bulk IP update action for node allocations (#1845)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com>
Co-authored-by: Charles <charles@pelican.dev>
2025-11-08 16:53:12 -05:00
exefer
1ff965611e Fix typo in DNS help text (#1868)
Authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-11-08 22:40:23 +01:00
Boy132
cec141889a Allow admins to "lock" allocations (#1811) 2025-11-08 21:54:41 +01:00
Charles
6ed84b5584 Add wings diagnostics retrieving to Edit Node page (#1865)
Co-authored-by: Boy132 <mail@boy132.de>
2025-11-08 15:47:40 -05:00
Lance Pioch
49f24e37b6 Laravel 12.37.0 Shift (#1864)
Co-authored-by: Shift <shift@laravelshift.com>
2025-11-06 08:43:02 -05:00
Boy132
e0c4e47a6c Fix directAccessibleServers returning duplicates (#1862) 2025-11-05 16:19:03 +01:00
Boy132
4bda7cba75 Allow to "embed" server list (#1860) 2025-11-05 16:18:44 +01:00
Boy132
852f7beb39 Allow to register "special file" alert banners (#1861) 2025-11-04 12:48:18 +01:00
mristau
d61583cd7b add server description to grid view too (#1851) 2025-11-04 06:03:50 -05:00
Charles
21f9f259d0 Add Egg Images (#1849) 2025-11-03 12:32:11 -05:00
M41den
b2aff5445b Fix admin serverlist search (#1854) 2025-11-03 06:50:08 -05:00
Boy132
1f26750a2a Add api endpoint for updating username (#1826) 2025-11-03 08:31:07 +01:00
Charles
6d83c6d908 composer update (#1856) 2025-11-02 18:53:24 -05:00
Copilot
574a391e73 Add border-radius to activity log avatars (#1848)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com>
2025-11-02 15:13:36 -05:00
PalmarHealer
605fcbe61a feat: Add mixed navigation type with admin-configurable defaults (#1850) 2025-10-31 14:12:54 -04:00
Letter N
0214b127e4 Add setup wizard to all oauth providers (#1801) 2025-10-31 14:09:20 -04:00
MartinOscar
e6aa76ef2c Refactor: add FilamentServiceProvider & globally make Select native(false) (#1836) 2025-10-29 23:23:18 +01:00
Boy132
d38075e3cb Add boolean cast to read_only toggle buttons (#1844) 2025-10-28 16:06:33 +01:00
M41den
0fec6adc3e Fix 500 "No route found" when creating db host (#1841) 2025-10-28 08:48:46 -04:00
M41den
5e3c22ea5e Fix weird postgres behavior when selecting mounts (#1842)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-10-28 08:48:35 -04:00
MartinOscar
d1a808a746 Hide User reset password Action on create Operation (#1840) 2025-10-28 01:38:37 +01:00
MartinOscar
3bcdeea800 Leverage user() helper (#1832) 2025-10-26 16:24:34 +01:00
Charles
e6bd6e416f Add archive extension selection (#1828) 2025-10-24 12:39:30 -04:00
Boy132
8e006ac32d Fix user permissions service (#1819) 2025-10-22 16:00:51 +02:00
Boy132
430f28a847 Add "cancel" button to profile (#1821) 2025-10-22 16:00:31 +02:00
Charles
1a4fa5e67a Replace Xtermjs canvas with webgl (#1807)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-10-14 20:35:26 -04:00
Charles
a65469b33b Remove duplicate translation entries (#1812) 2025-10-14 06:58:33 -04:00
Charles
d587cf3ee5 composer update (#1806) 2025-10-13 17:34:21 -04:00
Boy132
2cd9fa2cde Only keep the last 120 stored stats (#1805) 2025-10-13 22:50:16 +02:00
MartinOscar
d735e858a2 Rename Create actions in EditProfile (#1804) 2025-10-13 00:58:22 +02:00
MartinOscar
317fa46894 Use tenantMiddleware instead of manually fetching tenant query param (#1799) 2025-10-12 18:07:10 +02:00
Letter N
e589f972fb Add changelog preview when a new update is available (#1792)
Co-authored-by: Boy132 <mail@boy132.de>
2025-10-11 21:34:38 -04:00
MartinOscar
266e3779d5 Fix 500 when oauth is null (#1798) 2025-10-11 22:06:51 +02:00
MartinOscar
4652680a7b Add cpu helper on EditServer & move helperText to hintIcon on Create (#1795) 2025-10-10 22:46:47 +02:00
JoanFo
e99f7179c6 Topbar removed if using sidebar (#1789)
Co-authored-by: Boy132 <mail@boy132.de>
2025-10-10 16:37:14 -04:00
124 changed files with 3448 additions and 858 deletions

1
.gitignore vendored
View File

@@ -21,6 +21,7 @@ yarn-error.log
/.idea
/.nova
/.vscode
/.ddev
public/assets/manifest.json
/database/*.sqlite*

View File

@@ -18,7 +18,7 @@ enum CustomizationKey: string
self::ConsoleFont => 'monospace',
self::ConsoleFontSize => 14,
self::ConsoleGraphPeriod => 30,
self::TopNavigation => false,
self::TopNavigation => config('panel.filament.default-navigation', 'sidebar'),
self::DashboardLayout => 'grid',
};
}

View File

@@ -56,8 +56,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
->default(fn () => $server->image)
->notIn(fn () => $server->image)
->required()
->preload()
->native(false),
->preload(),
])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) {
try {

View File

@@ -4,6 +4,10 @@ namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Authentik\Provider;
final class AuthentikSchema extends OAuthSchema
@@ -20,11 +24,27 @@ final class AuthentikSchema extends OAuthSchema
public function getServiceConfig(): array
{
return [
return array_merge(parent::getServiceConfig(), [
'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'),
'client_id' => env('OAUTH_AUTHENTIK_CLIENT_ID'),
'client_secret' => env('OAUTH_AUTHENTIK_CLIENT_SECRET'),
];
]);
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Create Authentik Application')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>On your Authentik dashboard select <b>Applications</b>, then select <b>Create with Provider</b>.</p><p>On the creation step select <b>OAuth2/OpenID Provider</b> and on the configure step set <b>Redirect URIs/Origins</b> to the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/authentik')),
]),
], parent::getSetupSteps());
}
public function getSettingsForm(): array

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class BitbucketSchema extends OAuthSchema
{
public function getId(): string
{
return 'bitbucket';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Bitbucket Consumer')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud" target="_blank">Bitbucket OAuth Documentation</x-filament::link> and follow the steps in <b>Create a consumer</b>.</p><p>For the <b>Callback URL</b> use the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/bitbucket')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-bitbucket-f';
}
public function getHexColor(): string
{
return '#205081';
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class FacebookSchema extends OAuthSchema
{
public function getId(): string
{
return 'facebook';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Facebook Application')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://developers.facebook.com/apps" target="_blank">Facebook Developer Dashboard</x-filament::link> and select or create a new app you will use for authentication. Make sure to have "Authenticate and request data from users with Facebook Login" as one of the Use Cases.</p><p>Once selected go to <b>Use Cases</b> and customize "Authenticate and request data from users with Facebook Login", from there go to <b>Settings</b> and add <b>Valid OAuth Redirect URIs</b> using the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Valid OAuth Redirect URIs')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/facebook')),
TextEntry::make('get_app_info')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>To obtain the OAuth values go to <b>App Settings > Basic</b>.</p>'))),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-facebook-f';
}
public function getHexColor(): string
{
return '#1877f2';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class GoogleSchema extends OAuthSchema
{
public function getId(): string
{
return 'google';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new OAuth client')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://console.developers.google.com/" target="_blank">Google API Console</x-filament::link> and create or select the project you want to use.</p><p>Navigate or search <b>Credentials</b>, click on the <b>Create Credentials</b> button and select <b>OAuth client ID</b>. On the Application type select <b>Web Application</b>.</p><p>On <b>Authorized JavaScript origins</b> and <b>Authorized redirect URIs</b> add and use the values below.</p>'))),
TextInput::make('_noenv_origin')
->label('Authorized JavaScript origins')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('')),
TextInput::make('_noenv_callback')
->label('Authorized redirect URIs')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/google')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>When you filled all fields click on <b>Create</b>.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-google-f';
}
public function getHexColor(): string
{
return '#4285f4';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class LinkedinSchema extends OAuthSchema
{
public function getId(): string
{
return 'linkedin';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Obtain Linkedin App OAuth Config')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p><x-filament::link href="https://www.linkedin.com/developers/apps/new" target="_blank">Create</x-filament::link> or <x-filament::link href="https://www.linkedin.com/developers/apps" target="_blank">select</x-filament::link> the one you will be using for authentication.</p><p>Select the <b>Auth</b> tab and set <b>Authorized redirect URLs for your app</b> to the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorized redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/linkedin')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-linkedin-f';
}
public function getHexColor(): string
{
return '#0a66c2';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class SlackSchema extends OAuthSchema
{
public function getId(): string
{
return 'slack';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Slack OAuth')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p><x-filament::link href="https://api.slack.com/apps?new_app=1" target="_blank">Create</x-filament::link> a slack app or <x-filament::link href="https://api.slack.com/apps" target="_blank">select</x-filament::link> the one you will be using for authentication.</p><p>Navigate to the <b>OAuth & Permissions</b> section and configure the <b>Redirect URL</b> using the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/slack')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-slack';
}
public function getHexColor(): string
{
return '#6ecadc';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class XSchema extends OAuthSchema
{
public function getId(): string
{
return 'x';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new X App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://developer.x.com/en/portal/dashboard" target="_blank">X Developer Dashboard</x-filament::link> and create or select the project app you want to use.</p><p>Go to the app\'s settings and set up <b>User authentication</b> if not yet. Make sure to select <b>Web App</b> as the type of app.</p><p>For the <b>Callback URI / Redirect URL</b> and <b>Website URL</b> set it using the value below.</p>'))),
TextInput::make('_noenv_origin')
->label('Website URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('')),
TextInput::make('_noenv_callback')
->label('Callback URI / Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/x')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>If you have already set this up go to your app\'s <b>Keys and tokens</b> and obtain the Client ID and Secret there.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-x';
}
public function getHexColor(): string
{
return '#1da1f2';
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Filament\Admin\Pages;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Actions\ViewLogAction;
use Boquizo\FilamentLogViewer\Pages\ListLogs as BaseListLogs;
use Boquizo\FilamentLogViewer\Tables\Columns\LevelColumn;
use Boquizo\FilamentLogViewer\Tables\Columns\NameColumn;
use Boquizo\FilamentLogViewer\Utils\Level;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Http;
class ListLogs extends BaseListLogs
{
protected string $view = 'filament.components.list-logs';
public function getHeading(): string|null|\Illuminate\Contracts\Support\Htmlable
{
return trans('admin/log.navigation.panel_logs');
}
public static function table(Table $table): Table
{
return parent::table($table)
->emptyStateHeading(trans('admin/log.empty_table'))
->emptyStateIcon('tabler-check')
->columns([
NameColumn::make('date'),
LevelColumn::make(Level::ALL)
->tooltip(trans('admin/log.total_logs')),
LevelColumn::make(Level::Error)
->tooltip(trans('admin/log.error')),
LevelColumn::make(Level::Warning)
->tooltip(trans('admin/log.warning')),
LevelColumn::make(Level::Notice)
->tooltip(trans('admin/log.notice')),
LevelColumn::make(Level::Info)
->tooltip(trans('admin/log.info')),
LevelColumn::make(Level::Debug)
->tooltip(trans('admin/log.debug')),
])
->recordActions([
ViewLogAction::make()
->icon('tabler-file-description')->iconSize(IconSize::Medium),
DownloadAction::make()
->icon('tabler-file-download')->iconSize(IconSize::Medium),
Action::make('uploadLogs')
->button()
->hiddenLabel()
->icon('tabler-world-upload')->iconSize(IconSize::Medium)
->requiresConfirmation()
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn ($record) => trans('admin/log.actions.upload_logs_description', ['file' => $record['date'], 'url' => 'https://logs.pelican.dev']))
->action(function ($record) {
$logPath = storage_path('logs/' . $record['date']);
if (!file_exists($logPath)) {
Notification::make()
->title(trans('admin/log.actions.log_not_found'))
->body(trans('admin/log.actions.log_not_found_description', ['filename' => $record['date']]))
->danger()
->send();
return;
}
$lines = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$totalLines = count($lines);
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body(trans('admin/log.actions.failed_to_upload_description', ['status' => $response->status()]))
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/log.actions.log_upload'))
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body($e->getMessage())
->danger()
->send();
return;
}
}),
DeleteAction::make()
->icon('tabler-trash')->iconSize(IconSize::Medium),
]);
}
}

View File

@@ -181,7 +181,6 @@ class Settings extends Page implements HasSchemas
->schema([
Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider'))
->native(false)
->options($this->avatarService->getMappings())
->selectablePlaceholder(false)
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
@@ -204,6 +203,15 @@ class Settings extends Page implements HasSchemas
])
->stateCast(new BooleanStateCast(false, true))
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
ToggleButtons::make('FILAMENT_DEFAULT_NAVIGATION')
->label(trans('admin/setting.general.default_navigation'))
->inline()
->options([
'sidebar' => trans('admin/setting.general.sidebar'),
'topbar' => trans('admin/setting.general.topbar'),
'mixed' => trans('admin/setting.general.mixed'),
])
->default(env('FILAMENT_DEFAULT_NAVIGATION', config('panel.filament.default-navigation'))),
ToggleButtons::make('APP_2FA_REQUIRED')
->label(trans('admin/setting.general.2fa_requirement'))
->inline()
@@ -217,7 +225,6 @@ class Settings extends Page implements HasSchemas
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
Select::make('FILAMENT_WIDTH')
->label(trans('admin/setting.general.display_width'))
->native(false)
->options(Width::class)
->selectablePlaceholder(false)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Filament\Admin\Pages;
use App\Traits\ResolvesRecordDate;
use Boquizo\FilamentLogViewer\Actions\BackAction;
use Boquizo\FilamentLogViewer\Actions\DeleteAction;
use Boquizo\FilamentLogViewer\Actions\DownloadAction;
use Boquizo\FilamentLogViewer\Pages\ViewLog as BaseViewLog;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Facades\Http;
class ViewLogs extends BaseViewLog
{
use ResolvesRecordDate;
public function getHeaderActions(): array
{
return [
DeleteAction::make(withTooltip: true)
->icon('tabler-trash')->iconSize(IconSize::Medium),
DownloadAction::make(withTooltip: true)
->icon('tabler-file-download')->iconSize(IconSize::Medium),
Action::make('uploadLogs')
->button()
->hiddenLabel()
->icon('tabler-world-upload')->iconSize(IconSize::Medium)
->requiresConfirmation()
->tooltip(trans('admin/log.actions.upload_tooltip', ['url' => 'logs.pelican.dev']))
->modalHeading(trans('admin/log.actions.upload_logs'))
->modalDescription(fn () => trans('admin/log.actions.upload_logs_description', ['file' => $this->resolveRecordDate(), 'url' => 'https://logs.pelican.dev']))
->action(function () {
$logPath = storage_path('logs/' . $this->resolveRecordDate());
if (!file_exists($logPath)) {
Notification::make()
->title(trans('admin/log.actions.log_not_found'))
->body(trans('admin/log.actions.log_not_found_description', ['filename' => $this->resolveRecordDate()]))
->danger()
->send();
return;
}
$lines = file($logPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$totalLines = count($lines);
$uploadLines = $totalLines <= 1000 ? $lines : array_slice($lines, -1000);
$content = implode("\n", $uploadLines);
$logUrl = 'https://logs.pelican.dev';
try {
$response = Http::timeout(10)->asMultipart()->post($logUrl, [
[
'name' => 'c',
'contents' => $content,
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body(trans('admin/log.actions.failed_to_upload_description', ['status' => $response->status()]))
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/log.actions.log_upload'))
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
->label(trans('admin/log.actions.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/log.actions.failed_to_upload'))
->body($e->getMessage())
->danger()
->send();
return;
}
}),
BackAction::make()
->icon('tabler-arrow-left')->iconSize(IconSize::Medium),
];
}
}

View File

@@ -257,7 +257,6 @@ class CreateEgg extends CreateRecord
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->native(false)
->selectablePlaceholder(false)
->default('bash')
->options([

View File

@@ -16,6 +16,7 @@ use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
@@ -24,13 +25,19 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Flex;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Image;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\IconSize;
use Illuminate\Validation\Rules\Unique;
class EditEgg extends EditRecord
@@ -50,36 +57,215 @@ class EditEgg extends EditRecord
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon('tabler-egg')
->schema([
Grid::make(2)
->columnSpan(1)
->schema([
Image::make('', '')
->hidden(fn ($record) => !$record->image)
->url(fn ($record) => $record->image)
->alt('')
->alignJustify()
->imageSize(150)
->columnSpanFull(),
Flex::make([
Action::make('uploadImage')
->iconButton()
->iconSize(IconSize::Large)
->icon('tabler-photo-up')
->modal()
->modalHeading('')
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('base64Image'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
return;
}
try {
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new \Exception(trans('admin/egg.import.invalid_url'));
}
$allowedExtensions = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
];
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, $allowedExtensions)) {
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', $allowedExtensions)]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new \Exception(trans('admin/egg.import.no_local_ip'));
}
$context = stream_context_create([
'http' => ['timeout' => 3],
'https' => [
'timeout' => 3,
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$imageContent = @file_get_contents($state, false, $context, 0, 1048576); // 1024KB
if (!$imageContent) {
throw new \Exception(trans('admin/egg.import.image_error'));
}
if (strlen($imageContent) >= 1048576) {
throw new \Exception(trans('admin/egg.import.image_too_large'));
}
$mimeType = $allowedExtensions[$extension];
$base64 = 'data:' . $mimeType . ';base64,' . base64_encode($imageContent);
$set('base64Image', $base64);
$set('image_url_error', null);
} catch (\Exception $e) {
$set('image_url_error', $e->getMessage());
$set('base64Image', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn ($get) => $get('image_url_error') !== null)
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(1024)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->saveUploadedFileUsing(function ($file, Set $set) {
$base64 = "data:{$file->getMimeType()};base64,". base64_encode(file_get_contents($file->getRealPath()));
$set('base64Image', $base64);
return $base64;
}),
]),
]),
])
->action(function (array $data, $record): void {
$base64 = $data['base64Image'] ?? null;
if (empty($base64) && !empty($data['image'])) {
$base64 = $data['image'];
}
if (!empty($base64)) {
$record->update([
'image' => $base64,
]);
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
$record->refresh();
} else {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('deleteImage')
->visible(fn ($record) => $record->image)
->label('')
->icon('tabler-trash')
->iconButton()
->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
$record->update([
'image' => null,
]);
Notification::make()
->title(trans('admin/egg.import.image_deleted'))
->success()
->send();
$record->refresh();
}),
]),
]),
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 1])
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
->helperText(trans('admin/egg.description_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->columnSpan(1)
->disabled(),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->disabled(),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.description_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
@@ -93,24 +279,20 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
@@ -248,7 +430,6 @@ class EditEgg extends EditRecord
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->native(false)
->selectablePlaceholder(false)
->options([
'bash' => 'bash',

View File

@@ -19,6 +19,7 @@ use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ReplicateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
@@ -42,6 +43,13 @@ class ListEggs extends ListRecords
TextColumn::make('id')
->label('Id')
->hidden(),
ImageColumn::make('image')
->label('')
->alignCenter()
->circular()
->getStateUsing(fn ($record) => $record->image
? $record->image
: 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')))),
TextColumn::make('name')
->label(trans('admin/egg.name'))
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)

View File

@@ -24,6 +24,7 @@ use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -125,6 +126,7 @@ class MountResource extends Resource
ToggleButtons::make('read_only')
->label(trans('admin/mount.read_only'))
->helperText(trans('admin/mount.read_only_help'))
->stateCast(new BooleanStateCast(false, true))
->options([
false => trans('admin/mount.toggles.writable'),
true => trans('admin/mount.toggles.read_only'),
@@ -138,8 +140,7 @@ class MountResource extends Resource
true => 'success',
])
->inline()
->default(false)
->required(),
->default(false),
TextInput::make('source')
->label(trans('admin/mount.source'))
->required()
@@ -162,7 +163,8 @@ class MountResource extends Resource
Section::make()->schema([
Select::make('eggs')->multiple()
->label(trans('admin/mount.eggs'))
->relationship('eggs', 'name')
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
->preload(),
Select::make('nodes')->multiple()
->label(trans('admin/mount.nodes'))

View File

@@ -4,7 +4,7 @@ namespace App\Filament\Admin\Resources\Nodes\Pages;
use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Models\Node;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Repositories\Daemon\DaemonSystemRepository;
use App\Services\Helpers\SoftwareVersionService;
use App\Services\Nodes\NodeAutoDeployService;
use App\Services\Nodes\NodeUpdateService;
@@ -14,6 +14,8 @@ use Exception;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Slider;
use Filament\Forms\Components\Slider\Enums\PipsMode;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
@@ -25,6 +27,7 @@ use Filament\Resources\Pages\EditRecord;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
@@ -33,7 +36,10 @@ use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconSize;
use Filament\Support\RawJs;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
use Phiki\Grammar\Grammar;
use Throwable;
@@ -45,13 +51,13 @@ class EditNode extends EditRecord
protected static string $resource = NodeResource::class;
private DaemonConfigurationRepository $daemonConfigurationRepository;
private DaemonSystemRepository $daemonSystemRepository;
private NodeUpdateService $nodeUpdateService;
public function boot(DaemonConfigurationRepository $daemonConfigurationRepository, NodeUpdateService $nodeUpdateService): void
public function boot(DaemonSystemRepository $daemonSystemRepository, NodeUpdateService $nodeUpdateService): void
{
$this->daemonConfigurationRepository = $daemonConfigurationRepository;
$this->daemonSystemRepository = $daemonSystemRepository;
$this->nodeUpdateService = $nodeUpdateService;
}
@@ -624,6 +630,154 @@ class EditNode extends EditRecord
])->fullWidth(),
]),
]),
Tab::make('diagnostics')
->label(trans('admin/node.tabs.diagnostics'))
->icon('tabler-heart-search')
->schema([
Section::make('diag')
->heading(trans('admin/node.tabs.diagnostics'))
->columnSpanFull()
->columns(4)
->disabled(fn (Get $get) => $get('pulled'))
->headerActions([
Action::make('pull')
->label(trans('admin/node.diagnostics.pull'))
->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge)
->hidden(fn (Get $get) => $get('pulled'))
->action(function (Get $get, Set $set, Node $node) {
$includeEndpoints = $get('include_endpoints') ?? true;
$includeLogs = $get('include_logs') ?? true;
$logLines = $get('log_lines') ?? 200;
try {
$response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs);
if ($response->status() === 404) {
Notification::make()
->title(trans('admin/node.diagnostics.404'))
->warning()
->send();
return;
}
$set('pulled', true);
$set('uploaded', false);
$set('log', $response->body());
Notification::make()
->title(trans('admin/node.diagnostics.logs_pulled'))
->success()
->send();
} catch (ConnectionException $e) {
Notification::make()
->title(trans('admin/node.error_connecting', ['node' => $node->name]))
->body($e->getMessage())
->danger()
->send();
}
}),
Action::make('upload')
->label(trans('admin/node.diagnostics.upload'))
->visible(fn (Get $get) => $get('pulled') ?? false)
->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge)
->action(function (Get $get, Set $set) {
try {
$response = Http::asMultipart()->post('https://logs.pelican.dev', [
[
'name' => 'c',
'contents' => $get('log'),
],
[
'name' => 'e',
'contents' => '14d',
],
]);
if ($response->failed()) {
Notification::make()
->title(trans('admin/node.diagnostics.upload_failed'))
->body(fn () => $response->status() . ' - ' . $response->body())
->danger()
->send();
return;
}
$data = $response->json();
$url = $data['url'];
Notification::make()
->title(trans('admin/node.diagnostics.logs_uploaded'))
->body("{$url}")
->success()
->actions([
Action::make('viewLogs')
->label(trans('admin/node.diagnostics.view_logs'))
->url($url)
->openUrlInNewTab(true),
])
->persistent()
->send();
$set('log', $url);
$set('pulled', false);
$set('uploaded', true);
} catch (\Exception $e) {
Notification::make()
->title(trans('admin/node.diagnostics.upload_failed'))
->body($e->getMessage())
->danger()
->send();
}
}),
Action::make('clear')
->label(trans('admin/node.diagnostics.clear'))
->visible(fn (Get $get) => $get('pulled') ?? false)
->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger')
->action(function (Get $get, Set $set) {
$set('pulled', false);
$set('uploaded', false);
$set('log', null);
$this->refresh();
}
),
])
->schema([
ToggleButtons::make('include_endpoints')
->hintIcon('tabler-question-mark')->inline()
->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint'))
->formatStateUsing(fn () => 1)
->boolean(),
ToggleButtons::make('include_logs')
->live()
->hintIcon('tabler-question-mark')->inline()
->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint'))
->formatStateUsing(fn () => 1)
->boolean(),
Slider::make('log_lines')
->columnSpan(2)
->hiddenLabel()
->live()
->tooltips(RawJs::make(<<<'JS'
`${$value} lines`
JS))
->visible(fn (Get $get) => $get('include_logs'))
->range(minValue: 100, maxValue: 500)
->pips(PipsMode::Steps, density: 10)
->step(50)
->formatStateUsing(fn () => 200)
->fillTrack(),
Hidden::make('pulled'),
Hidden::make('uploaded'),
]),
Textarea::make('log')
->hiddenLabel()
->columnSpanFull()
->rows(35)
->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)),
]),
]),
]);
}
@@ -681,7 +835,7 @@ class EditNode extends EditRecord
try {
if ($changed) {
$this->daemonConfigurationRepository->setNode($node)->update($node);
$this->daemonSystemRepository->setNode($node)->update($node);
}
parent::getSavedNotification()?->send();
} catch (ConnectionException) {

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Nodes\RelationManagers;
use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
use App\Filament\Components\Actions\UpdateNodeAllocations;
use App\Models\Allocation;
use App\Models\Node;
use App\Services\Allocations\AssignmentService;
@@ -80,7 +81,9 @@ class AllocationsRelationManager extends RelationManager
->searchable()
->label(trans('admin/node.table.ip')),
])
->headerActions([
->toolbarActions([
DeleteBulkAction::make()
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
Action::make('create new allocation')
->label(trans('admin/node.create_allocation'))
->schema(fn () => [
@@ -118,9 +121,8 @@ class AllocationsRelationManager extends RelationManager
->required(),
])
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
])
->groupedBulkActions([
DeleteBulkAction::make()
UpdateNodeAllocations::make()
->nodeRecord($this->getOwnerRecord())
->authorize(fn () => user()?->can('update', $this->getOwnerRecord())),
]);
}

View File

@@ -116,6 +116,14 @@ class CreateServer extends CreateRecord
->prefixIcon('tabler-server-2')
->selectablePlaceholder(false)
->default(function () {
$lastUsedNode = session()->get('last_utilized_node');
if ($lastUsedNode && user()?->accessibleNodes()->where('id', $lastUsedNode)->exists()) {
$this->node = Node::find($lastUsedNode);
return $this->node?->id;
}
/** @var ?Node $latestNode */
$latestNode = user()?->accessibleNodes()->latest()->first();
$this->node = $latestNode;
@@ -518,12 +526,12 @@ class CreateServer extends CreateRecord
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/server.cpu_limit'))->inlineLabel()
->suffix('%')
->hintIcon('tabler-question-mark', trans('admin/server.cpu_helper'))
->default(0)
->required()
->columnSpan(2)
->numeric()
->minValue(0)
->helperText(trans('admin/server.cpu_helper')),
->minValue(0),
]),
Grid::make()
->columns(4)
@@ -829,6 +837,8 @@ class CreateServer extends CreateRecord
$data['allocation_additional'] = collect($allocation_additional)->filter()->all();
}
session()->put('last_utilized_node', $data['node_id']);
try {
return $this->serverCreationService->handle($data);
} catch (Exception $exception) {

View File

@@ -258,6 +258,7 @@ class EditServer extends EditRecord
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/server.cpu_limit'))->inlineLabel()
->suffix('%')
->hintIcon('tabler-question-mark', trans('admin/server.cpu_helper'))
->required()
->columnSpan(2)
->numeric()

View File

@@ -16,6 +16,7 @@ use Filament\Tables\Columns\SelectColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class ListServers extends ListRecords
{
@@ -47,7 +48,9 @@ class ListServers extends ListRecords
->searchable(),
TextColumn::make('name')
->label(trans('admin/server.name'))
->searchable()
->searchable(query: fn (Builder $query, string $search) => $query->where(
Server::query()->qualifyColumn('name'), 'like', "%{$search}%")
)
->sortable(),
TextColumn::make('node.name')
->label(trans('admin/server.node'))

View File

@@ -11,6 +11,7 @@ use Filament\Actions\AssociateAction;
use Filament\Actions\CreateAction;
use Filament\Actions\DissociateAction;
use Filament\Actions\DissociateBulkAction;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
@@ -60,16 +61,35 @@ class AllocationsRelationManager extends RelationManager
->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')),
IconColumn::make('is_locked')
->label(trans('admin/server.locked'))
->tooltip(trans('admin/server.locked_helper'))
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->recordActions([
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),
Action::make('lock')
->label(trans('admin/server.lock'))
->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => true]) && $this->deselectAllTableRecords())
->hidden(fn (Allocation $allocation) => $allocation->is_locked),
Action::make('unlock')
->label(trans('admin/server.unlock'))
->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => false]) && $this->deselectAllTableRecords())
->visible(fn (Allocation $allocation) => $allocation->is_locked),
DissociateAction::make()
->after(function (Allocation $allocation) {
$allocation->update(['notes' => null]);
$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
$allocation->update([
'notes' => null,
'is_locked' => false,
]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
}
}),
])
->headerActions([
@@ -107,6 +127,8 @@ class AllocationsRelationManager extends RelationManager
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))))
->splitKeys(['Tab', ' ', ','])
->required(),
Hidden::make('is_locked')
->default(true),
])
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
AssociateAction::make()
@@ -116,13 +138,25 @@ class AllocationsRelationManager extends RelationManager
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
->recordSelectSearchColumns(['ip', 'port'])
->label(trans('admin/server.add_allocation'))
->after(fn (array $data) => !$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]])),
->after(function (array $data) {
Allocation::whereIn('id', array_values(array_unique($data['recordId'])))->update(['is_locked' => true]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]]);
}
}),
])
->groupedBulkActions([
DissociateBulkAction::make()
->after(function () {
Allocation::whereNull('server_id')->update(['notes' => null]);
$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
Allocation::whereNull('server_id')->update([
'notes' => null,
'is_locked' => false,
]);
if (!$this->getOwnerRecord()->allocation_id) {
$this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
}
}),
]);
}

View File

@@ -81,6 +81,7 @@ class DatabasesRelationManager extends RelationManager
ViewAction::make()
->color('primary'),
DeleteAction::make()
->successNotificationTitle(null)
->using(function (Database $database, DatabaseManagementService $service) {
try {
$service->delete($database);

View File

@@ -188,7 +188,7 @@ class UserResource extends Resource
->hintAction(
Action::make('password_reset')
->label(trans('admin/user.password_reset'))
->hidden(fn () => config('mail.default', 'log') === 'log')
->hidden(fn (string $operation) => $operation === 'create' || config('mail.default', 'log') === 'log')
->icon('tabler-send')
->action(function (User $user) {
$status = Password::broker(Filament::getPanel('app')->getAuthPasswordBroker())->sendResetLink([
@@ -236,8 +236,7 @@ class UserResource extends Resource
->default(fn () => config('app.timezone', 'UTC'))
->selectablePlaceholder(false)
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
->searchable()
->native(false),
->searchable(),
Select::make('language')
->label(trans('profile.language'))
->columnSpan([
@@ -251,8 +250,7 @@ class UserResource extends Resource
->default('en')
->searchable()
->selectablePlaceholder(false)
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->native(false),
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
FileUpload::make('avatar')
->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false)
->avatar()
@@ -286,7 +284,7 @@ class UserResource extends Resource
return;
}
$actions = [];
foreach ($user->oauth as $schema => $_) {
foreach ($user->oauth ?? [] as $schema => $_) {
$schema = $oauthService->get($schema);
if (!$schema) {
return;
@@ -414,7 +412,7 @@ class UserResource extends Resource
$sshKey->delete();
Activity::event('user:ssh-key.delete')
->actor(auth()->user())
->actor(user())
->subject($user)
->subject($sshKey)
->property('fingerprint', $sshKey->fingerprint)

View File

@@ -43,7 +43,18 @@ class UpdateWidget extends FormWidget
->iconColor('warning')
->schema([
TextEntry::make('info')
->hiddenLabel()
->state(trans('admin/dashboard.sections.intro-update-available.content', ['latestVersion' => $this->softwareVersionService->latestPanelVersion()])),
Section::make(trans('admin/dashboard.sections.intro-update-available.button_changelog'))
->icon('tabler-script')
->collapsible()
->collapsed()
->schema([
TextEntry::make('changelog')
->hiddenLabel()
->state($this->softwareVersionService->latestPanelVersionChangelog())
->markdown(),
]),
])
->headerActions([
Action::make('update')

View File

@@ -10,10 +10,17 @@ class ServerResource extends Resource
{
protected static ?string $model = Server::class;
protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker';
protected static ?string $slug = '/';
protected static bool $shouldRegisterNavigation = false;
public static function getNavigationBadge(): ?string
{
return (string) user()?->directAccessibleServers()->where('owner_id', user()?->id)->count();
}
public static function canAccess(): bool
{
return true;
@@ -25,4 +32,10 @@ class ServerResource extends Resource
'index' => ListServers::route('/'),
];
}
public static function embedServerList(bool $condition = true): void
{
static::$slug = $condition ? null : '/';
static::$shouldRegisterNavigation = $condition;
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace App\Filament\Components\Actions;
use App\Models\Allocation;
use App\Models\Node;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
class UpdateNodeAllocations extends Action
{
public static function getDefaultName(): ?string
{
return 'bulk_update_ip';
}
protected function setUp(): void
{
parent::setUp();
$this->label(trans('admin/node.bulk_update_ip'));
$this->icon('tabler-replace');
$this->color('warning');
$this->requiresConfirmation();
$this->modalHeading(trans('admin/node.bulk_update_ip'));
$this->modalDescription(trans('admin/node.bulk_update_ip_description'));
$this->modalIconColor('warning');
$this->modalSubmitActionLabel(trans('admin/node.update_ip'));
$this->schema(function () {
/** @var Node $node */
$node = $this->record;
$currentIps = Allocation::where('node_id', $node->id)
->pluck('ip')
->unique()
->values()
->all();
return [
Select::make('old_ip')
->label(trans('admin/node.old_ip'))
->options(array_combine($currentIps, $currentIps))
->selectablePlaceholder(false)
->required()
->live(),
Select::make('new_ip')
->label(trans('admin/node.new_ip'))
->options(fn () => array_combine($node->ipAddresses(), $node->ipAddresses()) ?: [])
->required()
->different('old_ip'),
];
});
$this->action(function (array $data) {
/** @var Node $node */
$node = $this->record;
$allocations = Allocation::where('node_id', $node->id)->where('ip', $data['old_ip'])->get();
if ($allocations->count() === 0) {
Notification::make()
->title(trans('admin/node.no_allocations_to_update'))
->warning()
->send();
return;
}
$updated = 0;
$failed = 0;
foreach ($allocations as $allocation) {
try {
$allocation->update(['ip' => $data['new_ip']]);
$updated++;
} catch (Exception $exception) {
$failed++;
report($exception);
}
}
Notification::make()
->title(trans('admin/node.ip_updated', ['count' => $updated, 'total' => $allocations->count()]))
->body($failed > 0 ? trans('admin/node.ip_update_failed', ['count' => $failed]) : null)
->status($failed > 0 ? 'warning' : 'success')
->persistent()
->send();
});
}
public function nodeRecord(Node $node): static
{
$this->record = $node;
return $this;
}
}

View File

@@ -21,8 +21,6 @@ class CopyFrom extends Select
$this->searchable();
$this->native(false);
$this->live();
}

View File

@@ -34,7 +34,6 @@ use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
@@ -132,8 +131,7 @@ class EditProfile extends BaseEditProfile
->default(config('app.timezone', 'UTC'))
->selectablePlaceholder(false)
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
->searchable()
->native(false),
->searchable(),
Select::make('language')
->label(trans('profile.language'))
->required()
@@ -143,8 +141,7 @@ class EditProfile extends BaseEditProfile
->selectablePlaceholder(false)
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? ''
: trans('profile.language_help', ['state' => $state]) . ' <u><a href="https://crowdin.com/project/pelican-dev/">Update On Crowdin</a></u>'))
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->native(false),
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
FileUpload::make('avatar')
->visible(fn () => config('panel.filament.uploadable-avatars'))
->avatar()
@@ -236,7 +233,7 @@ class EditProfile extends BaseEditProfile
->columnSpanFull(),
])
->headerActions([
Action::make('create')
Action::make('create_api_key')
->label(trans('filament-actions::create.single.modal.actions.create.label'))
->disabled(fn (Get $get) => empty($get('description')))
->successRedirectUrl(self::getUrl(['tab' => 'api-keys::data::tab'], panel: 'app'))
@@ -325,7 +322,7 @@ class EditProfile extends BaseEditProfile
->live(),
])
->headerActions([
Action::make('create')
Action::make('create_ssh_key')
->label(trans('filament-actions::create.single.modal.actions.create.label'))
->disabled(fn (Get $get) => empty($get('name')) || empty($get('public_key')))
->successRedirectUrl(self::getUrl(['tab' => 'ssh-keys::data::tab'], panel: 'app'))
@@ -442,10 +439,10 @@ class EditProfile extends BaseEditProfile
->label(trans('profile.navigation'))
->inline()
->options([
1 => trans('profile.top'),
0 => trans('profile.side'),
])
->stateCast(new BooleanStateCast(false, true)),
'sidebar' => trans('profile.sidebar'),
'topbar' => trans('profile.topbar'),
'mixed' => trans('profile.mixed'),
]),
]),
Section::make(trans('profile.console'))
->collapsible()
@@ -555,6 +552,7 @@ class EditProfile extends BaseEditProfile
{
return [
$this->getSaveFormAction()->formId('form'),
$this->getCancelFormAction()->formId('form'),
];
}
@@ -584,7 +582,14 @@ class EditProfile extends BaseEditProfile
$data['console_rows'] = (int) $this->getUser()->getCustomization(CustomizationKey::ConsoleRows);
$data['console_graph_period'] = (int) $this->getUser()->getCustomization(CustomizationKey::ConsoleGraphPeriod);
$data['dashboard_layout'] = $this->getUser()->getCustomization(CustomizationKey::DashboardLayout);
$data['top_navigation'] = (bool) $this->getUser()->getCustomization(CustomizationKey::TopNavigation);
// Handle migration from boolean to string navigation types
$topNavigation = $this->getUser()->getCustomization(CustomizationKey::TopNavigation);
if (is_bool($topNavigation)) {
$data['top_navigation'] = $topNavigation ? 'topbar' : 'sidebar';
} else {
$data['top_navigation'] = $topNavigation;
}
return $data;
}

View File

@@ -51,7 +51,7 @@ class Startup extends ServerFormPage
->label(trans('server/startup.command'))
->live()
->visible(fn (Server $server) => in_array($server->startup, $server->egg->startup_commands))
->disabled(fn (Server $server) => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->disabled(fn (Server $server) => !user()?->can(Permission::ACTION_STARTUP_UPDATE, $server))
->formatStateUsing(fn (Server $server) => $server->startup)
->afterStateUpdated(function ($state, Server $server, Set $set) {
$original = $server->startup;

View File

@@ -73,15 +73,22 @@ class AllocationResource extends Resource
->action(fn (Allocation $allocation) => user()?->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server) && $server->update(['allocation_id' => $allocation->id]))
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label(trans('server/network.primary')),
IconColumn::make('is_locked')
->label(trans('server/network.locked'))
->tooltip(trans('server/network.locked_helper'))
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->recordActions([
DetachAction::make()
->visible(fn (Allocation $allocation) => !$allocation->is_locked || user()?->can('update', $allocation->node))
->authorize(fn () => user()?->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label(trans('server/network.delete'))
->icon('tabler-trash')
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
Allocation::where('id', $allocation->id)->update([
'notes' => null,
'is_locked' => false,
'server_id' => null,
]);
@@ -93,12 +100,12 @@ class AllocationResource extends Resource
->after(fn (Allocation $allocation) => $allocation->id === $server->allocation_id && $server->update(['allocation_id' => $server->allocations()->first()?->id])),
])
->toolbarActions([
Action::make('addAllocation')
Action::make('add_allocation')
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->icon(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'tabler-network-off' : 'tabler-network')
->authorize(fn () => user()?->can(Permission::ACTION_ALLOCATION_CREATE, $server))
->tooltip(fn () => $server->allocations()->count() >= $server->allocation_limit ? trans('server/network.limit') : trans('server/network.add'))
->hidden(fn () => !config('panel.client_features.allocations.enabled'))
->hidden(fn () => !config('panel.client_features.allocations.enabled') || $server->allocation === null)
->disabled(fn () => $server->allocations()->count() >= $server->allocation_limit)
->color(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'danger' : 'primary')
->action(function (FindAssignableAllocationService $service) use ($server) {

View File

@@ -230,6 +230,7 @@ class BackupResource extends Resource
->disabled(fn (Backup $backup) => $backup->is_locked && $backup->status !== BackupStatus::Failed)
->modalDescription(fn (Backup $backup) => trans('server/backup.actions.delete.description', ['backup' => $backup->name]))
->modalSubmitActionLabel(trans('server/backup.actions.delete.title'))
->successNotificationTitle(null)
->action(function (Backup $backup, DeleteBackupService $deleteBackupService) {
try {
$deleteBackupService->handle($backup);
@@ -265,6 +266,7 @@ class BackupResource extends Resource
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
->createAnother(false)
->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge)
->successNotificationTitle(null)
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
$action = $initiateBackupService->setIgnoredFiles(explode(PHP_EOL, $data['ignored'] ?? ''));

View File

@@ -138,6 +138,7 @@ class DatabaseResource extends Resource
ViewAction::make()
->modalHeading(fn (Database $database) => trans('server/database.viewing', ['database' => $database->database])),
DeleteAction::make()
->successNotificationTitle(null)
->using(function (Database $database, DatabaseManagementService $service) {
try {
$service->delete($database);
@@ -164,6 +165,7 @@ class DatabaseResource extends Resource
->disabled(fn () => $server->databases()->count() >= $server->database_limit)
->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary')
->createAnother(false)
->successNotificationTitle(null)
->schema([
Grid::make()
->columns(2)

View File

@@ -7,11 +7,13 @@ use App\Exceptions\Repository\FileNotEditableException;
use App\Facades\Activity;
use App\Filament\Server\Resources\Files\FileResource;
use App\Livewire\AlertBanner;
use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Closure;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\CodeEditor;
@@ -215,13 +217,15 @@ class EditFiles extends Page
$this->previousUrl = url()->previous();
if (str($path)->endsWith('.pelicanignore')) {
AlertBanner::make('.pelicanignore_info')
->title(trans('server/file.alerts.pelicanignore.title'))
->body(trans('server/file.alerts.pelicanignore.body'))
->info()
->closable()
->send();
foreach (File::getSpecialFiles() as $fileName => $data) {
if ($data['check'] instanceof Closure && $data['check']($path)) {
AlertBanner::make($fileName . '_info')
->title($data['title'])
->body($data['body'])
->info()
->closable()
->send();
}
}
}

View File

@@ -12,8 +12,10 @@ use App\Models\File;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Services\Nodes\NodeJWTService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Carbon\CarbonImmutable;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@@ -25,15 +27,14 @@ use Filament\Actions\EditAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Panel;
use Filament\Resources\Pages\ListRecords;
use Filament\Resources\Pages\PageRegistration;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Support\Enums\IconSize;
use Filament\Support\Facades\FilamentView;
@@ -41,7 +42,7 @@ use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Enums\PaginationMode;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\UploadedFile;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Routing\Route;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Route as RouteFacade;
@@ -54,6 +55,8 @@ class ListFiles extends ListRecords
protected static string $resource = FileResource::class;
protected string $view = 'filament.server.pages.list-files';
#[Locked]
public string $path = '/';
@@ -297,13 +300,26 @@ class ListFiles extends ListRecords
->label(trans('server/file.actions.archive.title'))
->icon('tabler-archive')->iconSize(IconSize::Large)
->schema([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
Grid::make(3)
->schema([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->columnSpan(2),
Select::make('extension')
->label(trans('server/file.actions.archive.extension'))
->selectablePlaceholder(false)
->options([
'tar.gz' => 'tar.gz',
'zip' => 'zip',
'tar.bz2' => 'tar.bz2',
'tar.xz' => 'tar.xz',
])
->columnSpan(1),
]),
])
->action(function ($data, File $file) {
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, [$file->name], $data['name']);
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, [$file->name], $data['name'], $data['extension']);
Activity::event('server:file.compress')
->property('name', $archive['name'])
@@ -392,15 +408,28 @@ class ListFiles extends ListRecords
BulkAction::make('archive')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_ARCHIVE, $server))
->schema([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
Grid::make(3)
->schema([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->columnSpan(2),
Select::make('extension')
->label(trans('server/file.actions.archive.extension'))
->selectablePlaceholder(false)
->options([
'tar.gz' => 'tar.gz',
'zip' => 'zip',
'tar.bz2' => 'tar.bz2',
'tar.xz' => 'tar.xz',
])
->columnSpan(1),
]),
])
->action(function ($data, Collection $files) {
$files = $files->map(fn ($file) => $file['name'])->toArray();
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name']);
$archive = $this->getDaemonFileRepository()->compressFiles($this->path, $files, $data['name'], $data['extension']);
Activity::event('server:file.compress')
->property('name', $archive['name'])
@@ -476,7 +505,7 @@ class ListFiles extends ListRecords
->color('primary')
->action(function ($data) {
try {
$this->getDaemonFileRepository()->createDirectory($data['name'], $this->path);
$this->createFolder($data['name']);
Activity::event('server:file.create-directory')
->property(['directory' => $this->path, 'name' => $data['name']])
@@ -500,58 +529,30 @@ class ListFiles extends ListRecords
->label(trans('server/file.actions.new_folder.folder_name'))
->required(),
]),
Action::make('upload')
Action::make('uploadFile')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_CREATE, $server))
->hiddenLabel()->icon('tabler-upload')->iconButton()->iconSize(IconSize::ExtraLarge)
->tooltip(trans('server/file.actions.upload.title'))
->view('filament.server.pages.file-upload'),
Action::make('uploadURL')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_CREATE, $server))
->hiddenLabel()->icon('tabler-download')->iconButton()->iconSize(IconSize::ExtraLarge)
->tooltip(trans('server/file.actions.upload.from_url'))
->modalHeading(trans('server/file.actions.upload.from_url'))
->color('success')
->action(function ($data) {
if (count($data['files']) > 0 && !isset($data['url'])) {
/** @var UploadedFile $file */
foreach ($data['files'] as $file) {
$this->getDaemonFileRepository()->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
$this->getDaemonFileRepository()->pull($data['url'], $this->path);
Activity::event('server:file.uploaded')
->property('directory', $this->path)
->property('file', $file->getClientOriginalName())
->log();
}
} elseif ($data['url'] !== null) {
$this->getDaemonFileRepository()->pull($data['url'], $this->path);
Activity::event('server:file.pull')
->property('url', $data['url'])
->property('directory', $this->path)
->log();
}
Activity::event('server:file.pull')
->property('url', $data['url'])
->property('directory', $this->path)
->log();
$this->refreshPage();
})
->schema([
Tabs::make()
->contained(false)
->schema([
Tab::make('files')
->label(trans('server/file.actions.upload.from_files'))
->live()
->schema([
FileUpload::make('files')
->storeFiles(false)
->previewable(false)
->preserveFilenames()
->maxSize((int) round($server->node->upload_size * (config('panel.use_binary_prefix') ? 1.048576 * 1024 : 1000)))
->multiple(),
]),
Tab::make('url')
->label(trans('server/file.actions.upload.url'))
->live()
->disabled(fn (Get $get) => count($get('files')) > 0)
->schema([
TextInput::make('url')
->label(trans('server/file.actions.upload.url'))
->url(),
]),
]),
TextInput::make('url')
->label(trans('server/file.actions.upload.url'))
->required()
->url(),
]),
Action::make('search')
->authorize(fn () => user()?->can(Permission::ACTION_FILE_READ, $server))
@@ -599,6 +600,81 @@ class ListFiles extends ListRecords
};
}
public function getUploadUrl(NodeJWTService $jwtService): string
{
/** @var Server $server */
$server = Filament::getTenant();
if (!user()?->can(Permission::ACTION_FILE_CREATE, $server)) {
abort(403, 'You do not have permission to upload files.');
}
$token = $jwtService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setUser(user())
->setClaims(['server_uuid' => $server->uuid])
->handle($server->node, user()->id . $server->uuid);
return sprintf(
'%s/upload/file?token=%s',
$server->node->getConnectionAddress(),
$token->toString()
);
}
public function getUploadSizeLimit(): int
{
/** @var Server $server */
$server = Filament::getTenant();
return $server->node->upload_size * 1024 * 1024;
}
/**
* @throws ConnectionException
* @throws FileExistsException
* @throws \Throwable
*/
public function createFolder(string $folderPath): void
{
/** @var Server $server */
$server = Filament::getTenant();
if (!user()?->can(Permission::ACTION_FILE_CREATE, $server)) {
abort(403, 'You do not have permission to create folders.');
}
try {
$this->getDaemonFileRepository()->createDirectory($folderPath, $this->path);
Activity::event('server:file.create-directory')
->property(['directory' => $this->path, 'name' => $folderPath])
->log();
} catch (FileExistsException) {
// Ignore if the folder already exists.
} catch (ConnectionException $e) {
Notification::make()
->body($e->getMessage())
->danger()
->send();
}
}
/**
* @param string[] $files
*/
public function logUploadedFiles(array $files): void
{
$filesCollection = collect($files);
Activity::event('server:files.uploaded')
->property('directory', $this->path)
->property('files', $filesCollection)
->log();
}
private function getDaemonFileRepository(): DaemonFileRepository
{
/** @var Server $server */

View File

@@ -255,8 +255,7 @@ class ScheduleResource extends Resource
'6' => trans('server/schedule.time.saturday'),
'0' => trans('server/schedule.time.sunday'),
])
->selectablePlaceholder(false)
->native(false),
->selectablePlaceholder(false),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');

View File

@@ -140,6 +140,7 @@ class UserResource extends Resource
DeleteAction::make()
->label(trans('server/user.delete'))
->hidden(fn (User $user) => user()?->id === $user->id)
->successNotificationTitle(null)
->action(function (User $user, SubuserDeletionService $subuserDeletionService) use ($server) {
$subuser = $server->subusers->where('user_id', $user->id)->first();
$subuserDeletionService->handle($subuser, $server);
@@ -154,6 +155,7 @@ class UserResource extends Resource
->hidden(fn (User $user) => user()?->id === $user->id)
->authorize(fn () => user()?->can(Permission::ACTION_USER_UPDATE, $server))
->modalHeading(fn (User $user) => trans('server/user.editing', ['user' => $user->email]))
->successNotificationTitle(null)
->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) {
$subuser = $server->subusers->where('user_id', $user->id)->first();

View File

@@ -121,11 +121,11 @@ class ServerConsole extends Widget
foreach ($data as $key => $value) {
$cacheKey = "servers.{$this->server->id}.$key";
$data = cache()->get($cacheKey, []);
$cachedStats = cache()->get($cacheKey, []);
$data[$timestamp] = $value;
$cachedStats[$timestamp] = $value;
cache()->put($cacheKey, $data, now()->addMinute());
cache()->put($cacheKey, array_slice($cachedStats, -120), now()->addMinute());
}
}

View File

@@ -77,7 +77,7 @@ class DatabaseHostController extends ApplicationApiController
return $this->fractal->item($databaseHost)
->transformWith($this->getTransformer(DatabaseHostTransformer::class))
->addMeta([
'resource' => route('api.application.databases.view', [
'resource' => route('api.application.databasehosts.view', [
'database_host' => $databaseHost->id,
]),
])

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\Client;
use App\Facades\Activity;
use App\Http\Requests\Api\Client\Account\UpdateEmailRequest;
use App\Http\Requests\Api\Client\Account\UpdatePasswordRequest;
use App\Http\Requests\Api\Client\Account\UpdateUsernameRequest;
use App\Services\Users\UserUpdateService;
use App\Transformers\Api\Client\UserTransformer;
use Illuminate\Auth\AuthManager;
@@ -36,6 +37,25 @@ class AccountController extends ClientApiController
->toArray();
}
/**
* Update username
*
* Update the authenticated user's username.
*/
public function updateUsername(UpdateUsernameRequest $request): JsonResponse
{
$original = $request->user()->username;
$this->updateService->handle($request->user(), $request->validated());
if ($original !== $request->input('username')) {
Activity::event('user:account.username-changed')
->property(['old' => $original, 'new' => $request->input('username')])
->log();
}
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* Update email
*

View File

@@ -212,7 +212,8 @@ class FileController extends ClientApiController
$file = $this->fileRepository->setServer($server)->compressFiles(
$request->input('root'),
$request->input('files'),
$request->input('name')
$request->input('name'),
$request->input('extension')
);
Activity::event('server:file.compress')

View File

@@ -5,6 +5,7 @@ namespace App\Http\Middleware\Activity;
use App\Facades\LogTarget;
use App\Models\Server;
use Closure;
use Filament\Facades\Filament;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -21,10 +22,7 @@ class ServerSubject
public function handle(Request $request, Closure $next): Response
{
$server = $request->route()->parameter('server');
if ($request->route()->hasParameter('tenant')) {
$server = Server::find($request->route()->parameter('tenant'));
}
$server ??= Filament::getTenant();
if ($server instanceof Server) {
LogTarget::setActor($request->user());

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests\Api\Client\Account;
use App\Exceptions\Http\Base\InvalidPasswordProvidedException;
use App\Http\Requests\Api\Client\ClientApiRequest;
use App\Models\User;
use Illuminate\Container\Container;
use Illuminate\Contracts\Hashing\Hasher;
class UpdateUsernameRequest extends ClientApiRequest
{
/**
* @throws InvalidPasswordProvidedException
*/
public function authorize(): bool
{
if (!parent::authorize()) {
return false;
}
$hasher = Container::getInstance()->make(Hasher::class);
// Verify password matches when changing password or email.
if (!$hasher->check($this->input('password'), $this->user()->password)) {
throw new InvalidPasswordProvidedException(trans('validation.internal.invalid_password'));
}
return true;
}
public function rules(): array
{
$rules = User::getRulesForUpdate($this->user());
return ['username' => $rules['username']];
}
}

View File

@@ -22,6 +22,7 @@ class CompressFilesRequest extends ClientApiRequest
'files' => 'required|array',
'files.*' => 'string',
'name' => 'sometimes|nullable|string',
'extension' => 'sometimes|in:zip,tgz,tar.gz,txz,tar.xz,tbz2,tar.bz2',
];
}
}

View File

@@ -34,7 +34,9 @@ class ProcessWebhook implements ShouldQueue
$data = reset($data);
}
$data = Arr::wrap(json_decode($data, true) ?? []);
if (is_string($data)) {
$data = Arr::wrap(json_decode($data, true) ?? []);
}
$data['event'] = $this->webhookConfiguration->transformClassName($this->eventName);
if ($this->webhookConfiguration->type === WebhookType::Discord) {
@@ -55,12 +57,13 @@ class ProcessWebhook implements ShouldQueue
}
try {
$customHeaders = $this->webhookConfiguration->headers;
$headers = [];
foreach ($customHeaders as $key => $value) {
$headers[$key] = $this->webhookConfiguration->replaceVars($data, $value);
}
if ($this->webhookConfiguration->type === WebhookType::Regular) {
foreach ($this->webhookConfiguration->headers as $key => $value) {
$headers[$key] = $this->webhookConfiguration->replaceVars($data, $value);
}
}
Http::withHeaders($headers)->post($this->webhookConfiguration->endpoint, $data)->throw();
$successful = now();
} catch (Exception $exception) {

View File

@@ -117,7 +117,6 @@ class PanelInstaller extends SimplePage implements HasForms
->selectablePlaceholder(false)
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->afterStateUpdated(fn ($state, Application $app) => $app->setLocale($state ?? config('app.locale')))
->native(false)
->columnStart(4);
}

View File

@@ -25,6 +25,18 @@ class ServerEntry extends Component
</div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
@if($server->egg->image)
<div style="
position: absolute;
inset: 0;
background: url('{{ $server->egg->image }}') right no-repeat;
background-size: contain;
opacity: 0.20;
max-width: 680px;
max-height: 140px;
"></div>
@endif
<div class="flex items-center mb-5 gap-2">
<x-filament::loading-indicator class="h-6 w-6" />
<h2 class="text-xl font-bold">

View File

@@ -165,7 +165,7 @@ class ActivityLog extends Model implements HasIcon, HasLabel
public function getIp(): ?string
{
return auth()->user()->can('seeIps activityLog') ? $this->ip : null;
return user()?->can('seeIps activityLog') ? $this->ip : null;
}
public function htmlable(): string
@@ -185,7 +185,7 @@ class ActivityLog extends Model implements HasIcon, HasLabel
return "
<div style='display: flex; align-items: center;'>
<img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px' />
<img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px; border-radius: 50%;' />
<div>
<p>$username$this->event</p>

View File

@@ -29,6 +29,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $address
* @property Server|null $server
* @property Node $node
* @property bool $is_locked
*
* @method static AllocationFactory factory(...$parameters)
* @method static Builder|Allocation newModelQuery()
@@ -55,6 +56,10 @@ class Allocation extends Model
*/
public const RESOURCE_NAME = 'allocation';
protected $attributes = [
'is_locked' => false,
];
/**
* Fields that are not mass assignable.
*/
@@ -68,10 +73,17 @@ class Allocation extends Model
'ip_alias' => ['nullable', 'string'],
'server_id' => ['nullable', 'exists:servers,id'],
'notes' => ['nullable', 'string', 'max:256'],
'is_locked' => ['boolean'],
];
protected static function booted(): void
{
static::updating(function (self $allocation) {
if (is_null($allocation->server_id)) {
$allocation->is_locked = false;
}
});
static::deleting(function (self $allocation) {
throw_if($allocation->server_id, new ServerUsingAllocationException(trans('exceptions.allocations.server_using')));
});
@@ -83,6 +95,7 @@ class Allocation extends Model
'node_id' => 'integer',
'port' => 'integer',
'server_id' => 'integer',
'is_locked' => 'bool',
];
}

View File

@@ -21,6 +21,7 @@ use Illuminate\Support\Str;
* @property string $author
* @property string $name
* @property string|null $description
* @property string|null $image
* @property string[]|null $features
* @property array<string, string> $docker_images
* @property string|null $update_url
@@ -80,6 +81,7 @@ class Egg extends Model implements Validatable
'name',
'author',
'description',
'image',
'features',
'docker_images',
'force_outgoing_ip',
@@ -104,6 +106,7 @@ class Egg extends Model implements Validatable
'uuid' => ['required', 'string', 'size:36'],
'name' => ['required', 'string', 'max:255'],
'description' => ['string', 'nullable'],
'image' => ['string', 'nullable'],
'features' => ['array', 'nullable'],
'author' => ['required', 'string', 'email'],
'file_denylist' => ['array', 'nullable'],

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Livewire\AlertBanner;
use App\Repositories\Daemon\DaemonFileRepository;
use Carbon\Carbon;
use Closure;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
@@ -54,6 +55,32 @@ class File extends Model
protected static ?string $searchTerm;
/** @var array<string, array<string, string|Closure|null>> */
protected static array $customSpecialFiles = [];
public static function registerSpecialFile(string $fileName, string|Closure $bannerTitle, string|Closure|null $bannerBody = null, ?Closure $nameCheck = null): void
{
static::$customSpecialFiles[$fileName] = [
'title' => $bannerTitle,
'body' => $bannerBody,
'check' => $nameCheck ?? fn (string $path) => str($path)->endsWith($fileName),
];
}
/** @return array<string, array<string, string|Closure|null>> */
public static function getSpecialFiles(): array
{
$specialFiles = [
'.pelicanignore' => [
'title' => fn () => trans('server/file.alerts.pelicanignore.title'),
'body' => fn () => trans('server/file.alerts.pelicanignore.body'),
'check' => fn (string $path) => str($path)->endsWith('.pelicanignore'),
],
];
return array_merge($specialFiles, static::$customSpecialFiles);
}
public static function get(Server $server, string $path = '/', ?string $searchTerm = null): Builder
{
self::$server = $server;

View File

@@ -4,7 +4,7 @@ namespace App\Models;
use App\Contracts\Validatable;
use App\Exceptions\Service\HasActiveServersException;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Repositories\Daemon\DaemonSystemRepository;
use App\Traits\HasValidation;
use Carbon\Carbon;
use Exception;
@@ -316,7 +316,7 @@ class Node extends Model implements Validatable
{
return once(function () {
try {
return (new DaemonConfigurationRepository())
return (new DaemonSystemRepository())
->setNode($this)
->getSystemInformation();
} catch (Exception $exception) {

View File

@@ -287,7 +287,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id')
->where(function (Builder $builder) {
$builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id);
});
})
->distinct('servers.id');
}
public function accessibleNodes(): Builder

View File

@@ -171,10 +171,14 @@ class WebhookConfiguration extends Model
}
/**
* @param array<mixed, mixed> $replacement
* @param array<mixed, mixed>|object $replacement
* */
public function replaceVars(array $replacement, string $subject): string
public function replaceVars(array|object $replacement, string $subject): string
{
if (is_object($replacement)) {
$replacement = $replacement->toArray();
}
return preg_replace_callback(
'/{{(.*?)}}/',
function ($matches) use ($replacement) {

View File

@@ -26,30 +26,18 @@ use App\Services\Helpers\SoftwareVersionService;
use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\TextInput\Actions\CopyAction;
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;
use Illuminate\Config\Repository;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Sanctum\Sanctum;
use Livewire\Component;
use Livewire\Livewire;
use Spatie\Health\Facades\Health;
use function Livewire\on;
use function Livewire\store;
class AppServiceProvider extends ServiceProvider
{
/**
@@ -104,57 +92,6 @@ class AppServiceProvider extends ServiceProvider
Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens);
FilamentColor::register([
'danger' => Color::Red,
'gray' => Color::Zinc,
'info' => Color::Sky,
'primary' => Color::Blue,
'success' => Color::Green,
'warning' => Color::Amber,
'blurple' => Color::hex('#5865F2'),
]);
FilamentView::registerRenderHook(
PanelsRenderHook::PAGE_START,
fn () => Blade::render('@livewire(\App\Livewire\AlertBannerContainer::class)'),
);
FilamentView::registerRenderHook(
PanelsRenderHook::FOOTER,
fn () => Blade::render('filament.layouts.footer'),
);
FilamentView::registerRenderHook(
PanelsRenderHook::STYLES_BEFORE,
fn () => Blade::render("@vite(['resources/css/app.css'])")
);
FilamentView::registerRenderHook(
PanelsRenderHook::SCRIPTS_AFTER,
fn () => Blade::render("@vite(['resources/js/app.js'])"),
);
on('dehydrate', function (Component $component) {
if (!Livewire::isLivewireRequest()) {
return;
}
if (store($component)->has('redirect')) {
return;
}
if (count(session()->get('alert-banners') ?? []) <= 0) {
return;
}
$component->dispatch('alertBannerSent');
});
Field::macro('hintCopy', function () {
/** @var Field $this */
return $this->hintAction(CopyAction::make()); // @phpstan-ignore varTag.nativeType
});
// Don't run any health checks during tests
if (!$app->runningUnitTests()) {
Health::checks([

View File

@@ -4,11 +4,16 @@ namespace App\Providers\Extensions;
use App\Extensions\OAuth\OAuthService;
use App\Extensions\OAuth\Schemas\AuthentikSchema;
use App\Extensions\OAuth\Schemas\CommonSchema;
use App\Extensions\OAuth\Schemas\BitbucketSchema;
use App\Extensions\OAuth\Schemas\DiscordSchema;
use App\Extensions\OAuth\Schemas\FacebookSchema;
use App\Extensions\OAuth\Schemas\GithubSchema;
use App\Extensions\OAuth\Schemas\GitlabSchema;
use App\Extensions\OAuth\Schemas\GoogleSchema;
use App\Extensions\OAuth\Schemas\LinkedinSchema;
use App\Extensions\OAuth\Schemas\SlackSchema;
use App\Extensions\OAuth\Schemas\SteamSchema;
use App\Extensions\OAuth\Schemas\XSchema;
use Illuminate\Support\ServiceProvider;
class OAuthServiceProvider extends ServiceProvider
@@ -19,14 +24,14 @@ class OAuthServiceProvider extends ServiceProvider
$service = new OAuthService();
// Default OAuth providers included with Socialite
$service->register(new CommonSchema('facebook', icon: 'tabler-brand-facebook-f', hexColor: '#1877f2'));
$service->register(new CommonSchema('x', icon: 'tabler-brand-x-f', hexColor: '#1da1f2'));
$service->register(new CommonSchema('linkedin', icon: 'tabler-brand-linkedin-f', hexColor: '#0a66c2'));
$service->register(new CommonSchema('google', icon: 'tabler-brand-google-f', hexColor: '#4285f4'));
$service->register(new FacebookSchema());
$service->register(new XSchema());
$service->register(new LinkedinSchema());
$service->register(new GoogleSchema());
$service->register(new GithubSchema());
$service->register(new GitlabSchema());
$service->register(new CommonSchema('bitbucket', icon: 'tabler-brand-bitbucket-f', hexColor: '#205081'));
$service->register(new CommonSchema('slack', icon: 'tabler-brand-slack', hexColor: '#6ecadc'));
$service->register(new BitbucketSchema());
$service->register(new SlackSchema());
// Additional OAuth providers from socialiteproviders.com
$service->register(new AuthentikSchema());

View File

@@ -2,7 +2,9 @@
namespace App\Providers\Filament;
use AchyutN\FilamentLogViewer\FilamentLogViewer;
use App\Filament\Admin\Pages\ListLogs;
use App\Filament\Admin\Pages\ViewLogs;
use Boquizo\FilamentLogViewer\FilamentLogViewerPlugin;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Navigation\NavigationGroup;
@@ -35,8 +37,11 @@ class AdminPanelProvider extends PanelProvider
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets')
->plugins([
FilamentLogViewer::make()
FilamentLogViewerPlugin::make()
->authorize(fn () => user()->can('view panelLog'))
->listLogs(ListLogs::class)
->viewLog(ViewLogs::class)
->navigationLabel(fn () => trans('admin/log.navigation.panel_logs'))
->navigationGroup(fn () => trans('admin/dashboard.advanced'))
->navigationIcon('tabler-file-info'),
]);

View File

@@ -2,7 +2,7 @@
namespace App\Providers\Filament;
use AchyutN\FilamentLogViewer\FilamentLogViewer;
use Boquizo\FilamentLogViewer\FilamentLogViewerPlugin;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Panel;
@@ -16,6 +16,7 @@ class AppPanelProvider extends PanelProvider
->default()
->breadcrumbs(false)
->navigation(false)
->topbar(true)
->userMenuItems([
Action::make('to_admin')
->label(trans('profile.admin'))
@@ -25,7 +26,7 @@ class AppPanelProvider extends PanelProvider
])
->discoverResources(in: app_path('Filament/App/Resources'), for: 'App\\Filament\\App\\Resources')
->plugins([
FilamentLogViewer::make()
FilamentLogViewerPlugin::make()
->authorize(false),
]);
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Providers\Filament;
use Filament\Forms\Components\Field;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput\Actions\CopyAction;
use Filament\Support\Colors\Color;
use Filament\Support\Facades\FilamentColor;
use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Livewire\Component;
use Livewire\Livewire;
use function Livewire\on;
use function Livewire\store;
class FilamentServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
FilamentColor::register([
'danger' => Color::Red,
'gray' => Color::Zinc,
'info' => Color::Sky,
'primary' => Color::Blue,
'success' => Color::Green,
'warning' => Color::Amber,
'blurple' => Color::hex('#5865F2'),
]);
FilamentView::registerRenderHook(
PanelsRenderHook::PAGE_START,
fn () => Blade::render('@livewire(\App\Livewire\AlertBannerContainer::class)'),
);
FilamentView::registerRenderHook(
PanelsRenderHook::FOOTER,
fn () => Blade::render('filament.layouts.footer'),
);
FilamentView::registerRenderHook(
PanelsRenderHook::STYLES_BEFORE,
fn () => Blade::render("@vite(['resources/css/app.css'])")
);
FilamentView::registerRenderHook(
PanelsRenderHook::SCRIPTS_AFTER,
fn () => Blade::render("@vite(['resources/js/app.js'])"),
);
on('dehydrate', function (Component $component) {
if (!Livewire::isLivewireRequest()) {
return;
}
if (store($component)->has('redirect')) {
return;
}
if (count(session()->get('alert-banners') ?? []) <= 0) {
return;
}
$component->dispatch('alertBannerSent');
});
Field::macro('hintCopy', function () {
/** @var Field $this */
return $this->hintAction(CopyAction::make()); // @phpstan-ignore varTag.nativeType
});
Select::configureUsing(fn (Select $select) => $select->native(false));
}
public function register(): void {}
}

View File

@@ -34,7 +34,16 @@ abstract class PanelProvider extends BasePanelProvider
->brandLogo(config('app.logo'))
->brandLogoHeight('2rem')
->favicon(config('app.favicon', '/pelican.ico'))
->topNavigation(fn () => user()?->getCustomization(CustomizationKey::TopNavigation))
->topNavigation(function () {
$navigationType = user()?->getCustomization(CustomizationKey::TopNavigation);
return $navigationType === 'topbar' || $navigationType === true;
})
->topbar(function () {
$navigationType = user()?->getCustomization(CustomizationKey::TopNavigation);
return $navigationType === 'topbar' || $navigationType === 'mixed' || $navigationType === true;
})
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
->profile(EditProfile::class, false)
->userMenuItems([

View File

@@ -41,7 +41,7 @@ class ServerPanelProvider extends PanelProvider
->discoverResources(in: app_path('Filament/Server/Resources'), for: 'App\\Filament\\Server\\Resources')
->discoverPages(in: app_path('Filament/Server/Pages'), for: 'App\\Filament\\Server\\Pages')
->discoverWidgets(in: app_path('Filament/Server/Widgets'), for: 'App\\Filament\\Server\\Widgets')
->middleware([
->tenantMiddleware([
ServerSubject::class,
]);
}

View File

@@ -153,7 +153,7 @@ class DaemonFileRepository extends DaemonRepository
*
* @throws ConnectionException
*/
public function compressFiles(?string $root, array $files, ?string $name): array
public function compressFiles(?string $root, array $files, ?string $name, ?string $extension): array
{
return $this->getHttpClient()
// Wait for up to 15 minutes for the archive to be completed when calling this endpoint
@@ -164,6 +164,7 @@ class DaemonFileRepository extends DaemonRepository
'root' => $root ?? '/',
'files' => $files,
'name' => $name ?? '',
'extension' => $extension ?? '',
]
)->json();
}

View File

@@ -6,7 +6,7 @@ use App\Models\Node;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
class DaemonConfigurationRepository extends DaemonRepository
class DaemonSystemRepository extends DaemonRepository
{
/**
* Returns system information from the daemon instance.
@@ -30,6 +30,23 @@ class DaemonConfigurationRepository extends DaemonRepository
})->json();
}
/**
* Retrieve diagnostics from the daemon for the current node.
*
*
* @throws ConnectionException
*/
public function getDiagnostics(int $lines, bool $includeEndpoints, bool $includeLogs): Response
{
return $this->getHttpClient()
->timeout(5)
->get('/api/diagnostics', [
'log_lines' => $lines,
'include_endpoints' => $includeEndpoints ? 'true' : 'false',
'include_logs' => $includeLogs ? 'true' : 'false',
]);
}
/**
* Updates the configuration information for a daemon. Updates the information for
* this instance using a passed-in model. This allows us to change plenty of information

View File

@@ -85,6 +85,7 @@ class AssignmentService
'port' => (int) $unit,
'ip_alias' => array_get($data, 'allocation_alias'),
'server_id' => $server->id ?? null,
'is_locked' => array_get($data, 'is_locked', false),
];
}
} else {
@@ -98,6 +99,7 @@ class AssignmentService
'port' => (int) $port,
'ip_alias' => array_get($data, 'allocation_alias'),
'server_id' => $server->id ?? null,
'is_locked' => array_get($data, 'is_locked', false),
];
}

View File

@@ -29,6 +29,7 @@ class EggExporterService
'author' => $egg->author,
'uuid' => $egg->uuid,
'description' => $egg->description,
'image' => $egg->image,
'tags' => $egg->tags,
'features' => $egg->features,
'docker_images' => $egg->docker_images,

View File

@@ -198,6 +198,7 @@ class EggImporterService
return $model->forceFill([
'name' => Arr::get($parsed, 'name'),
'description' => Arr::get($parsed, 'description'),
'image' => Arr::get($parsed, 'image'),
'tags' => Arr::get($parsed, 'tags', []),
'features' => Arr::get($parsed, 'features'),
'docker_images' => Arr::get($parsed, 'docker_images'),

View File

@@ -7,6 +7,24 @@ use Illuminate\Support\Facades\Http;
class SoftwareVersionService
{
public function latestPanelVersionChangelog(): string
{
$key = 'panel:latest_version_changelog';
if (cache()->get($key) === 'error') {
cache()->forget($key);
}
return cache()->remember($key, now()->addMinutes(config('panel.cdn.cache_time', 60)), function () {
try {
$response = Http::timeout(5)->connectTimeout(1)->get('https://api.github.com/repos/pelican-dev/panel/releases/latest')->throw()->json();
return $response['body'];
} catch (Exception) {
return 'error';
}
});
}
public function latestPanelVersion(): string
{
$key = 'panel:latest_version';

View File

@@ -4,7 +4,7 @@ namespace App\Services\Nodes;
use App\Exceptions\Service\Node\ConfigurationNotPersistedException;
use App\Models\Node;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Repositories\Daemon\DaemonSystemRepository;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Str;
@@ -17,7 +17,7 @@ class NodeUpdateService
*/
public function __construct(
private ConnectionInterface $connection,
private DaemonConfigurationRepository $configurationRepository,
private DaemonSystemRepository $configurationRepository,
) {}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Services\Servers;
use App\Models\Permission;
use App\Models\Server;
use App\Models\Subuser;
use App\Models\User;
@@ -17,23 +18,26 @@ class GetUserPermissionsService
*/
public function handle(Server $server, User $user): array
{
if ($user->id === $server->owner_id) {
$isOwner = $user->id === $server->owner_id;
$isAdmin = $user->isAdmin() && ($user->can('view', $server) || $user->can('update', $server));
if ($isOwner && !$isAdmin) {
return ['*'];
}
if ($user->isAdmin() && ($user->can('view', $server) || $user->can('update', $server))) {
$permissions = $user->can('update', $server) ? ['*'] : ['websocket.connect', 'backup.read'];
$adminPermissions = [
'admin.websocket.errors',
'admin.websocket.install',
'admin.websocket.transfer',
];
$permissions[] = 'admin.websocket.errors';
$permissions[] = 'admin.websocket.install';
$permissions[] = 'admin.websocket.transfer';
return $permissions;
if ($isAdmin) {
return $isOwner || $user->can('update', $server) ? array_merge(['*'], $adminPermissions) : array_merge([Permission::ACTION_WEBSOCKET_CONNECT], $adminPermissions);
}
/** @var Subuser|null $subuserPermissions */
$subuserPermissions = $server->subusers()->where('user_id', $user->id)->first();
/** @var Subuser|null $subuser */
$subuser = $server->subusers()->where('user_id', $user->id)->first();
return $subuserPermissions ? $subuserPermissions->permissions : [];
return $subuser->permissions ?? [];
}
}

View File

@@ -191,6 +191,7 @@ class ServerCreationService
->get()
->each(function (Allocation $allocation) use ($server) {
$allocation->server_id = $server->id;
$allocation->is_locked = true;
$allocation->save();
});
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Traits;
use Illuminate\Support\Arr;
trait ResolvesRecordDate
{
/**
* @param mixed|null $record
*/
protected function resolveRecordDate($record = null): ?string
{
$r = $record ?? ($this->record ?? null);
if (is_scalar($r)) {
return (string) $r;
}
if (is_array($r)) {
return Arr::get($r, 'date') !== null ? (string) Arr::get($r, 'date') : null;
}
if (is_object($r)) {
if (method_exists($r, 'getAttribute')) {
$val = $r->getAttribute('date');
if ($val !== null) {
return (string) $val;
}
}
if (isset($r->date) || property_exists($r, 'date')) {
return (string) $r->date;
}
if (method_exists($r, 'toArray')) {
$arr = $r->toArray();
return Arr::get($arr, 'date') !== null ? (string) Arr::get($arr, 'date') : null;
}
}
return null;
}
}

View File

@@ -46,6 +46,7 @@ class EggTransformer extends BaseTransformer
'name' => $model->name,
'author' => $model->author,
'description' => $model->description,
'image' => $model->image,
'features' => $model->features,
'tags' => $model->tags,
'docker_image' => Arr::first($model->docker_images, default: ''), // docker_images, use startup_commands

View File

@@ -5,13 +5,14 @@ return [
App\Providers\AppServiceProvider::class,
App\Providers\BackupsServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\Filament\AppPanelProvider::class,
App\Providers\Filament\ServerPanelProvider::class,
App\Providers\Extensions\AvatarServiceProvider::class,
App\Providers\Extensions\CaptchaServiceProvider::class,
App\Providers\Extensions\FeatureServiceProvider::class,
App\Providers\Extensions\OAuthServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
App\Providers\Filament\AppPanelProvider::class,
App\Providers\Filament\FilamentServiceProvider::class,
App\Providers\Filament\ServerPanelProvider::class,
App\Providers\RouteServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,
];

View File

@@ -9,13 +9,13 @@
"ext-mbstring": "*",
"ext-pdo": "*",
"ext-zip": "*",
"achyutn/filament-log-viewer": "^1.4",
"aws/aws-sdk-php": "^3.356",
"calebporzio/sushi": "^2.5",
"dedoc/scramble": "^0.12.10",
"filament/filament": "~4.0",
"gboquizosanchez/filament-log-viewer": "^2.1",
"guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.31",
"laravel/framework": "^12.37",
"laravel/helpers": "^1.7",
"laravel/sanctum": "^4.2",
"laravel/socialite": "^5.23",

836
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -53,6 +53,7 @@ return [
'display-width' => env('FILAMENT_WIDTH', 'screen-2xl'),
'avatar-provider' => env('FILAMENT_AVATAR_PROVIDER', 'gravatar'),
'uploadable-avatars' => env('FILAMENT_UPLOADABLE_AVATARS', false),
'default-navigation' => env('FILAMENT_DEFAULT_NAVIGATION', 'sidebar'),
],
'use_binary_prefix' => env('PANEL_USE_BINARY_PREFIX', true),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,11 +2,12 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-sponge.yaml'
exported_at: '2025-09-12T08:38:42+00:00'
exported_at: '2025-10-31T12:41:03+00:00'
name: Sponge
author: panel@example.com
uuid: f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d
description: 'A community-driven open source Minecraft: Java Edition modding platform.'
image: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBmaWxsPSIjRjdDRjBEIj48cGF0aCBkPSJNMTkwIDBIMTBDNC41IDAgMCA0LjUgMCAxMHYxODBjMCA1LjUgNC41IDEwIDEwIDEwaDE2LjFjLTEuNy00NS43LS4xLTUyLjUgMy4xLTU3IDMuOS01LjYgNS41LTYuMyAxMS40LTExIDUtNCAzLjItMTAuNS0uNC0xNS4yLTIuMi0yLjktNS4zLTYuMy03LjctOS42LTEuNS0yLjIgMi4yLTE1LjEgMy42LTE5LjggMS40LTQuNyAzLjgtMjAgMjQuOC0yNC4xIDcuOS0xLjYgMjkuNi0yLjcgNDQuNS0xLjgtLjEtLjYtLjMtMS4zLS40LTItLjMtMS4yLS41LTIuNS0uOC0zLjktLjMtMS4zLS42LTIuNy0uOS00LjEtLjMtMS40LS43LTIuOC0xLTQuMy0uNC0xLjUtLjctMi45LTEuMi00LjQtLjgtMy0xLjgtNS45LTMtOC43LS42LTEuNC0xLjItMi43LTEuOS0zLjktLjctMS4xLTEuNC0yLjEtMi0yLjUtLjEtLjEtLjItLjItLjMtLjJoLS4xLjJzLjEgMCAwIDBsLS4zLS4xaC0uMmwtLjQtLjFoLS41Yy0xLjMtLjEtMi43LS4xLTQuMiAwLTIuOS4yLTYgLjgtOSAxLjVzLTUuOSAxLjYtOC43IDIuNGMtMS4yLjQtMi4zLjgtMy40IDEuMS4xLjkuMiAxLjcuMiAyLjYgMCAxMy0xMC41IDIzLjUtMjMuNSAyMy41UzIwLjYgNDcuOSAyMC42IDM0LjlzMTAuNS0yMy41IDIzLjUtMjMuNWM4LjcgMCAxNi4zIDQuNyAyMC40IDExLjggMS0uNCAyLjEtLjggMy4yLTEuMiAyLjgtMS4xIDUuOS0yLjIgOS4xLTMuMiAzLjMtMSA2LjctMiAxMC41LTIuNSAxLjktLjMgMy45LS40IDYuMS0uM2guOGMuMyAwIC42LjEuOC4xSDk1LjdsLjMuMWguMWwuMy4xcy4yIDAgLjMuMWwuNC4xYy42LjIuOS4zIDEuMy41cy43LjMgMS4xLjVjLjcuNCAxLjMuOCAxLjkgMS4yIDEuMS45IDIgMS44IDIuNyAyLjcuOC45IDEuNCAxLjggMiAyLjcgMS4yIDEuOCAyLjEgMy41IDIuOSA1LjIgMS42IDMuNCAyLjkgNi44IDMuOSAxMGwxLjUgNC44Yy41IDEuNi44IDMuMSAxLjIgNC42LjIuNy40IDEuNS41IDIuMi4yLjcuMyAxLjQuNSAyLjEuMyAxLjQuNiAyLjguOSA0LjEuNCAyIC43IDMuOSAxIDUuNiAyMi40IDIuMiAzOS41IDUuMSA0Ny4yIDEyLjggMTEuMyAxMSAyMCA2MSAxNC4zIDEyNC41aDEwYzUuNSAwIDEwLTQuNSAxMC0xMFYxMGMwLTUuNS00LjUtMTAtMTAtMTB6Ii8+PHBhdGggZD0iTTkxLjQgMTQwLjhjLTEuMyAzLjYtMi40IDQ1LjcgMTAgNDUuN3MxMi41LTQzLjIgMTIuMS00NS43Yy0uNC0yLjQtMjAuOC0zLjUtMjIuMSAwek03NSAxMDBjLTguNS0xLjItMTMuNiA0MC4yLTEuNyA0Mi42IDExLjIgMi4yIDEwLjEtNDEuNCAxLjctNDIuNnpNMTMwLjggMTAwYy04LjUtMS4yLTEzLjYgNDAuMi0xLjcgNDIuNiAxMS4yIDIuMiAxMC4yLTQxLjQgMS43LTQyLjZ6Ii8+PC9zdmc+'
tags:
- minecraft
features:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,13 +2,14 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-custom-source-engine-game.yaml'
exported_at: '2025-09-05T08:55:22+00:00'
exported_at: '2025-10-31T12:43:00+00:00'
name: 'Custom Source Engine Game'
author: panel@example.com
uuid: 2a42d0c2-c0ba-4067-9a0a-9b95d77a3490
description: |-
This option allows modifying the startup arguments and other details to run a custom SRCDS based
game on the panel.
image: 'data:image/webp;base64,UklGRoAMAABXRUJQVlA4THMMAAAv+QATECUwbtvIkdx/2ZNnLnwjYgLYqi8KnDuYekQOBtxU1VNGcZdt8CWWtnXLbdOCTsNlE6Ix5eSRaVdUVr1HA0Dlx+NLkmTVtm3binROhla09D55Lsi/tN5reIlWJ3xHSKBt7dii9/uN8Cv8yLaxjGXbrrFt20YY2whj2zZy/bbVK8FtI0mS6JnZe9d1ZWVW7QskSZIcN1L9/zwbHDaPlYyAqtAy1NVBG0mOnCyW92E8+9FhG0mKVBkf/fjgKXVZkCSbttXXtm3btm3bfrZt27b5dW3btm3uuyBIkuSoyWLwSHjPBwr8a2Y+MUMVHtiDh/iHYtShBa1oQCUqUYv6V2h4HQO0ogXt6EbXK/Rj4BUGMfYOk5h5hwUsYRGLWMb6O2yBgGATs2jDT6RbYlG4eWMvZksTu/EbXVgF+bEwjZsQvbV53+lhjBtoBwXyw2EUAVfG8CEddaBAviBs48h9QRSXMAbyLeHAZc3HiwfHMAHyPWHLcrsqGKIT5KtCh8V1UWZ4XvNl4dBFIfbjOYYVdOE/bmEPwuEBB9jBGQFIxll8QC1mzkPXhP2h2DWzJtas8qjUohQoemVdDC7uoR8vEQrFf+dh2h+rP78/jSUCe5xHJTYOL+D8UDZVX+9qfHHqdc0oUMvrzcVgzwbm8I5xAlvumP153XRQx5mzt0Rcex7bwtelohao2/WqmPWhbl8Mo4D5JwzjBCNROXJjsSIQpQch86kIdaVohdSJ2lLPanOdv9kOiMfmGyhcsXirx25eQw8fNByDeoPhqa491cUKrpMVXp1RuFljT2M8RQ7ejD3tThy9cU4W7MPcIWj3xPpQuFpdlgVqVTEKFLvWR6GF5owics9A4zgnUxQ6aj4PRpwHdQJyotBUCMHCAbjXlSjACpN1hk9bogBDjFVhZM7w9iUKMKv+R9yPQmfBq/YsGlswaE0UsK8EuVFoLnyuQHh3IIK+PPRxcRe9ozYeiQUyAvSLNgYBGQn+R+Rv4gc0EtIb+mTjEJGRFvZFG7XxSEhwvyIv1EJgHi4XeAFH5bbr9E4vPjG+Pu912qoYa4itQlv1+GyQEE+vl85bIGYRs03i2IIDlbFrbTihPKFzaiUW4YKlUsd8wMX3XmD3f4S7Jh0e+G2E0Kgf7juobrkMvWGFmeuc1/rwcf310gkt3H5GCgyDn1lYg9rYG0aRW5RgDpmrUr9HjhYixhmi2MyYP1Yvh0QRLdkl5AZYZ27RXOvQW/ukle7agULC50llit5rfelXD5xi92jBFCp3lW7cGYrpPjKlZjRuSRuCFpaT8D89Aj0CBzGmk8sBT5eYz9VIrdjkTkU4VCiZZ72ObU9O9A475Ylz2FCpF2K8As/xTvLB94hqfecd4/J2KjAKniTBN2vlkYKzGBUbqSWZVfOGX6M8HIUDTztbnxzvdnIBAfrB7BeUhfTgJIzWbUyyKU6NmhmROgJVrObe8J3zYk1aMRTgNAQCA4vH1fDF/RV2wmb4C2ZfIPaB0zj2wHSYXg2LUSssMAm+pOBUct7w3oHzeAHXmPNCNUJhfwFJzoAHtoPz2N5sqtfjYhRiJ+CUgWVLMUk12JE7Y4ZShbvGcnfpQhPNV7KYImYaOjDGj824HaPNDAcawIiBlinXUHQB5XK++GjyJen3YiSMczKhJQE/opCks/OVZ2siytfOy4vF8B/smNzWLJIXPylW+A2GaL0dMzkYScbEC+idUyKIl3A1rtIHviF+Mi3da5xSLpQnP2k2Gg5N7C0wCC4nwC3pQTHUKQhdeJM0dHw+WZ8GW7mtGbJ1zOH0EH3bD463BAKfPPTYJ3wx6Tf5L761XQkGQhfVH7DxwlW//FqzZeNK+HcFDcDyMzSNgzlp+dH2I70CUhlteYu3PUEYTFZwqmYu2OPSX4LRRambdpl5CaLqDPMKdYVTdHo+2NJLePZmFZohuN/LODIADvSf+vxWFNijJlFmVN8jXDM/mAF2GnPhcNHT8PoBeDJ7M2vBTlnsa324rbEpgGGkD398YzOhtA3J5hQ4B78+wRyk6gwKDR5oli2L8CHMdrxZqExoGAbMvh+5yDAR9r3zoEDEO0wkxRRQ0ys4WCDH1V5H0wAOfYI3UcjilgY6Cq07fLRM4UTRmXoq5H7nzv1MLtAJYQrXc/ZAeUyEdwm53AIudFIO1HYuD8D5E9ikfYt4my97P7Sq57vIJ3J6KVo+n1I7G0S5my/v+MD43oWY/WGbWGKgX4Kx8Ij+92YoB8uJtvFvk8/mjBrW9lDx/7bSRSGdKeApTHp1WSIZNbGw829jTySF2wOfu1mYimnvPC8wDm+G/k846ZjjKY45Td3PEh5n4MPEHtKikPeO83C0TCJwRR36eae4W+BkO4RBQNrNjAmNi8jOoTwp4OuRdvbr/6dD9xbGx7mEopCo/GQczwa2F651zhyhEoW70c2MpGowPnRW5Km9gcZBV1Rt4VEUSrG0iw6cxbDcw+mg/VzXYGfCzXxMvd4IfnVm5Zl0AzMGkL8DyjKKQjH+9uLOtoVR/Qe3O21HLAx0Js55NuNugux/Z1qeyTewYADZOyiIQj00+dp8mPxxahFPrndOnX9A2Kkbe0e8mdGVIPjZWTSn4QbmDiBvB7EFbskbZrkqs9ULYvAQ3U8Od24dbTZNJyshR41gfJ1xPYH2rrNzTnaYsitXoHAWJVwHULH7KMviK3Bftr0Mu/2LaHkUsDSsRH9An4gHO34J1bKPEAy+rICusDA4gUNhfQkKrMAw02Bd4Lb8fyy06NjArSjcm01U7H36UF5AQbjkeEZ5HWEEvYDSjp53hEPxOts7A5t0YDElwSUU5m8ybh/shNzH4MLYCpvQKdk0tGvykYR3KjC7ALewijP+xM3OwwIF5IWiMiPp4OuUhGYSD9xJ6ydPegUtN/7XDSzvvoxU4j+u0c8QeIkQyyw2UWd+gQJehNXpn8jhPkQT54ISAosiO2/AgbRzNEpHmXp7pqvczRMxlpj/GYtVDsB2sxnBNXr7wMAlgvYtTREKWJAuuSycj/QCnvilx8A/otPUzOjU8EkUrbDZxed1gQI7iKFP5yVkb4Q+FFg1gJ0r9Mw5OGp0zW5K7Ak7GR/nA0kYSHjNZja6PwfkUBTU0H2DOA7eQX6o5jXrQCZ9ONXzFM7XbZc80H0yT8l9AJ9WOBeFGtM6IqFBOrPJsQ8Xi14JfT1vEUPHfHkV4WNa9Ktof8YZ8BYwnoVuHnLsz2NIwr7A+SAtNPM5nI+89VReaDCs6yGxMvZULuIQhhJcVQt3tUiWMB5ucptlKD2TdUk5L0zsHW522sGgk5eu9saHwp5ZqVmzRS6SgvcoIIU5vVewSRwOJm+1bqUm1l+qOR960Lighj8WJm/lzlCG48tzAPrYbvArCtVsTvvFN9tUSZOqyq44kZDTR/64Q6locw29i3UhDRzkLgVsyf3oKJnzk/RNvZ4UBvPylYbK2GHKPIAT62Z4lKH6DhZyoXWz6oVmZO3lzsJ4P8LowS02TieYjkpcPx+p5tVc/xE94P2iXV8honm8TuYolBOCP+fLsuvdbBwjlRGGyBFtim5RYFHcnKJLtzAFlFNebFqBBJiuqzgchQMJ9XfKS47xiRHVjG/CUzNbB1x7IEruUWCTtA5fFcrOM3T/ZtxELzCBZwvMW9JROBGGg7QTlDYsi9xNsDuTq8C2vCzaiC3MB8z2IBZQNnt7rXOy4Yr9PmN29bQAYquVfvAyz9IHvsAEZDC/wMcoHEqAfQZODNjK7VgNWprrVJkhu8/DuUyv4wDohfTLkMz4CLT5ydHewZXRdrMy2UfmfCxdk7yRczXBDhMeHlXZs9wKzIJr6+axp0MUjvWwlGKZSz7qxycmxtPnvfMWiWcxqvYVwlG9Vs/9xyUiNOyHm9bKYDkptrFirXXHb6NERAa9sk3845PnZbE9tvuj7fHxupNd74A53O6NFW6BK74aJiDG0+uVdo2chtUWQ7EdHvprlIgY118P7VCwaQWGgTxmFqiezxd9FI72J7LJ23axsbYN0ovdHoTNwGK/KRv9Sq93zKZjYrIs8FcFijX7xd42i/hiNTh0TCx0uFd64dW6ioQodBucQC3QaU+ubs0ZHjSvq0hvdxVP1kGLGfZuIQFkhcBuBzZYXuFvtwOtzdt+mIVKszjV0AfS9WFguBt8aXagiLHNoHmcU6BZeLAZTEE7Cs1C9ebn8XCNQrdQvMAGQqPQLpxaBhFR6Ne/c/Gg9gEZUeg4QwRvsQiC9rM/jCoAAA=='
tags:
- source
- steamcmd

View File

@@ -2,13 +2,14 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-garrys-mod.yaml'
exported_at: '2025-09-05T08:54:21+00:00'
exported_at: '2025-10-31T12:37:53+00:00'
name: 'Garrys Mod'
author: panel@example.com
uuid: 60ef81d4-30a2-4d98-ab64-f59c69e2f915
description: |-
Garrys Mod, is a sandbox physics game created by Garry Newman, and developed by his company,
Facepunch Studios.
image: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyNC4zLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAzODQgMzg0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAzODQgMzg0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDojMDA4MUZGO30NCgkuc3Qxe2ZpbGw6I0ZGRkZGRjt9DQo8L3N0eWxlPg0KPHBhdGggY2xhc3M9InN0MCIgZD0iTTM0My45MiwzODRjLTEwMS42LDAtMjAzLjIsMC0zMDQuOCwwYy0wLjg5LTAuNDQtMS44NC0wLjMxLTIuNzktMC4zOGMtNC41MS0wLjM2LTguNjEtMi4wNy0xMi41Ni00LjEzDQoJYy01LjktMy4wNy0xMC44NC03LjMxLTE0LjkyLTEyLjU0Yy0zLjkzLTUuMDQtNi43My0xMC42NS04LjE3LTE2LjljLTAuMzktMS42OC0wLjMxLTMuNDEtMC40MS01LjEyYy0wLjAyLTAuMzQsMC4wOS0wLjc0LTAuMjctMQ0KCUMwLDI0Mi42NCwwLDE0MS4zNiwwLDQwLjA4YzAuNDMtMC42NywwLjI1LTEuNDQsMC4yNy0yLjE1YzAuMTEtNC42NCwxLjU3LTguOTIsMy42My0xMi45N0M5LjIyLDE0LjQ4LDE3LjI1LDYuODgsMjguMjUsMi40Ng0KCWMzLjM2LTEuMzUsNi44MS0yLjA5LDEwLjQtMi4yN0MzOS4xMywwLjE2LDM5LjY3LDAuNiw0MC4wOCwwQzE0MS45MiwwLDI0My43NiwwLDM0NS42LDBjMC43MSwwLjc3LDEuNjgsMC4zNiwyLjQ5LDAuNDINCgljMS45MiwwLjE1LDMuNzQsMC42NCw1LjUzLDEuMjVjNy4zMSwyLjQ2LDEzLjU4LDYuNTYsMTguODIsMTIuMmM1LjM5LDUuOCw5LjEyLDEyLjUsMTAuOSwyMC4yNWMwLjM4LDEuNjYsMC4yOSwzLjM3LDAuMzksNS4wNg0KCWMwLjAyLDAuMzItMC4wNywwLjY3LDAuMjYsMC45YzAsMTAxLjI4LDAsMjAyLjU2LDAsMzAzLjg0Yy0wLjU4LDAuOC0wLjM1LDEuNzYtMC4zNCwyLjU5YzAuMDMsMi43LTAuNjMsNS4yNC0xLjUzLDcuNzENCgljLTQuMjMsMTEuNTUtMTEuODgsMjAuMTItMjIuOCwyNS43M2MtMy4xMywxLjYxLTYuNDIsMi45MS05Ljk2LDMuNDNjLTEuNTIsMC4yMi0zLjA0LDAuMjYtNC41NiwwLjMzDQoJQzM0NC40OCwzODMuNzMsMzQ0LjE1LDM4My42OCwzNDMuOTIsMzg0eiIvPg0KPHBhdGggY2xhc3M9InN0MSIgZD0iTTIzNS41NywyMjEuOTFjLTIuODQsMy40LTUuNjgsNy41MS05LjIyLDEwLjg4Yy03LjIsNi44NC0xNi4yLDEwLjQxLTI1Ljc3LDEyLjQ5DQoJYy0xMS41MywyLjUxLTIzLjEzLDIuMTgtMzQuNzYsMC42MWMtMTQuNTItMS45NS0yNy4yNy03LjgzLTM4LjM4LTE3LjIyYy05LjM1LTcuOTEtMTYuMDYtMTcuOC0yMC44NC0yOS4wNA0KCWMtNC44OC0xMS40Ni03LjQtMjMuNTEtOC42OC0zNS44MWMtMS44NS0xNy43OC0wLjk5LTM1LjQyLDQuMzMtNTIuNmM0LjM3LTE0LjExLDExLjAxLTI3LDIxLjA4LTM4LjAzDQoJQzEzNSw2MC40LDE0OS4xOCw1Mi4wOCwxNjYuMSw0OC41OWM5LjQ2LTEuOTUsMTkuMDEtMS45NSwyOC41Ny0wLjRjMTcsMi43NywzMC4xMywxMS40OSwzOS43NSwyNS42OGMwLjI1LDAuMzIsMS4xOSwxLjcyLDEuMTksMS43Mg0KCXMwLTQuNzgsMC02LjgzYy0wLjAxLTUuMTYsMC0xNi41MSwwLTE2LjUxczEuMDksMCwxLjM1LDBjMTUuMjgtMC4wNSw0OS45Ny0wLjAxLDQ5Ljk3LTAuMDFzLTAuMDEsMS45Ni0wLjAxLDQuMjgNCgljMCw2NC40LDAuMDksMTI4LjgtMC4wNSwxOTMuMTljLTAuMDQsMjAuMzYtNS43OSwzOS0xOC44NCw1NC45NWMtMTAuMTUsMTIuNC0yMy4yNSwyMC41Ny0zOC4yNSwyNS44NQ0KCWMtMTQuNiw1LjE0LTI5LjcsNi45MS00NS4wOCw2LjI2Yy0xNi44MS0wLjcxLTMyLjk5LTQuMzUtNDcuNzUtMTIuNjRjLTIyLjIyLTEyLjQ5LTM1LjcyLTMxLjIyLTM5LjA3LTU2Ljc4DQoJYy0wLjQ1LTMuMzktMC41Ni02Ljg0LTAuNjktMTAuMjZjLTAuMDEtMC4yMS0wLjA3LTEuMzEtMC4wNy0xLjMxczEuMDQsMCwxLjI2LDBjMTUuMjQtMC4wNywzMC40OC0wLjA4LDQ1LjcyLTAuMDMNCgljMC4xOCwwLDAuOSwwLjAxLDAuOSwwLjAxcy0wLjAxLDAuOTYtMC4wMSwxLjEyYzAuMTYsOC44NiwyLjYyLDE2Ljg2LDguNzcsMjMuNDRjNC41OSw0LjkxLDEwLjI2LDguMTYsMTYuNyw5Ljg5DQoJYzE0Ljk2LDQuMDMsMjkuNTksMy4xOSw0My40OS0zLjk4YzEyLjUtNi40NSwyMC40LTE2LjU0LDIxLjMtMzAuODVjMC42Ny0xMC43OSwwLjI3LTIxLjY2LDAuMzQtMzIuNDkNCglDMjM1LjU3LDIyMi4zNSwyMzUuNTcsMjIxLjc5LDIzNS41NywyMjEuOTF6Ii8+DQo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMTUwLjc3LDE0OS44MmMtMC4zLTExLjc5LDAuOTMtMjMuMzcsNS41NS0zNC4zNWM1LjQzLTEyLjg5LDE0LjkxLTIwLjc0LDI4LjktMjIuODgNCgljOS42Ny0xLjQ4LDE5LjA4LTAuODUsMjcuOTksMy40N2M4LjksNC4zMiwxNC4zOSwxMS43LDE3Ljk5LDIwLjY0YzMuNTMsOC43NSw1LjMzLDE3Ljk0LDUuNDYsMjcuMzFjMC4xMiw4LjM4LDAuMDksMTYuODktMS4yLDI1LjE0DQoJYy0yLjU5LDE2LjU3LTE0LjA5LDMyLjA4LTMxLjczLDM2LjRjLTE2LjA5LDMuOTQtMzIuNjktMi4yMy00Mi4yOC0xNS44MWMtNi43NC05LjUzLTkuODUtMjAuMjEtMTAuNjYtMzEuNjgNCglDMTUwLjYxLDE1NS4zMywxNTAuNzcsMTUyLjU3LDE1MC43NywxNDkuODJ6Ii8+DQo8L3N2Zz4NCg=='
tags:
- source
- steamcmd

File diff suppressed because one or more lines are too long

View File

@@ -2,13 +2,14 @@ _comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
meta:
version: PLCN_v3
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/source-engine/egg-team-fortress2.yaml'
exported_at: '2025-09-05T08:55:44+00:00'
exported_at: '2025-10-31T12:31:09+00:00'
name: 'Team Fortress 2'
author: panel@example.com
uuid: 7f8eb681-b2c8-4bf8-b9f4-d79ff70b6e5d
description: |-
Team Fortress 2 is a team-based first-person shooter multiplayer video game developed and published
by Valve Corporation. It is the sequel to the 1996 mod Team Fortress for Quake and its 1999 remake.
image: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iNTAwcHgiIGhlaWdodD0iNTAwLjAwOXB4IiB2aWV3Qm94PSItNTAgLTUwLjAwNSA1MDAgNTAwLjAwOSIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAtNTAgLTUwLjAwNSA1MDAgNTAwLjAwOSINCgkgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8bGluZWFyR3JhZGllbnQgaWQ9IlNWR0lEXzFfIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjI1NS4zOTk0IiB5MT0iLTQzMy44NDM4IiB4Mj0iMTQ0LjU5ODkiIHkyPSI1My44NDUxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDAgMTApIj4NCgk8c3RvcCAgb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojNUQxRjBFIi8+DQoJPHN0b3AgIG9mZnNldD0iMSIgc3R5bGU9InN0b3AtY29sb3I6I0QwOTczNyIvPg0KPC9saW5lYXJHcmFkaWVudD4NCjxwYXRoIGZpbGw9InVybCgjU1ZHSURfMV8pIiBkPSJNMjYyLjA0NC00Mi4yMzlDMzcwLjA5Ny0xNC42NDQsNDUwLDgzLjM0NCw0NTAsMTk5Ljk5OGMwLDIuNjYxLTAuMDU3LDUuMzExLTAuMTQzLDcuOTUxDQoJbC0xNjguMTUxLTIzLjkxNmMtNC45MjItMjUuMzM4LTIxLjMzMy00Ni41NTYtNDMuNTk0LTU4LjA0NUwyNjIuMDQ0LTQyLjIzOXogTTEyNS45OSwxNjEuODgNCgljMTEuNDg4LTIyLjI2MSwzMi43MDctMzguNjcsNTguMDQzLTQzLjU5M2wyMy45Mi0xNjguMTUzYy0yLjY0My0wLjA4My01LjI5LTAuMTM5LTcuOTUzLTAuMTM5DQoJYy0xMTYuNjUyLDAtMjE0LjYzOSw3OS44OTgtMjQyLjIzNSwxODcuOTUzTDEyNS45OSwxNjEuODh6IE0xNjEuODgzLDI3NC4wMDhjLTIyLjI1OS0xMS40ODktMzguNjctMzIuNzEtNDMuNTkyLTU4LjA0NQ0KCWwtMTY4LjE1Mi0yMy45MmMtMC4wODMsMi42NDMtMC4xMzksNS4yOTMtMC4xMzksNy45NTVjMCwxMTYuNjQ4LDc5Ljg5OCwyMTQuNjM3LDE4Ny45NTIsMjQyLjIzM0wxNjEuODgzLDI3NC4wMDh6IE0yNzQuMDEsMjM4LjExMw0KCWMtMTEuNDksMjIuMjYxLTMyLjcwNywzOC42NjktNTguMDQ2LDQzLjU5M2wtMjMuOTE5LDE2OC4xNThjMi42NDMsMC4wODMsNS4yOTIsMC4xNCw3Ljk1NCwwLjE0DQoJYzExNi42NTMsMCwyMTQuNjQtNzkuOTAxLDI0Mi4yMzItMTg3Ljk1NUwyNzQuMDEsMjM4LjExM3oiLz4NCjxyYWRpYWxHcmFkaWVudCBpZD0iU1ZHSURfMl8iIGN4PSI5OC4xOTE5IiBjeT0iLTE5OC4zNTYiIHI9IjQyNS45ODcxIiBmeD0iOTIuNzU2NSIgZnk9Ii0xOTkuNDgzNCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgwLjk3NzQgMC4yMTE0IDAuMTI2NiAtMC41ODUxIC03Ny4wMzAxIC0xNjcuMzcwNykiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCgk8c3RvcCAgb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojRkZGRkZGO3N0b3Atb3BhY2l0eTowIi8+DQoJPHN0b3AgIG9mZnNldD0iMC40NjU3IiBzdHlsZT0ic3RvcC1jb2xvcjojRkZGRkZGO3N0b3Atb3BhY2l0eTowLjA4MDQiLz4NCgk8c3RvcCAgb2Zmc2V0PSIwLjk4NjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNGRkZGRkY7c3RvcC1vcGFjaXR5OjAuMjE0MyIvPg0KCTxzdG9wICBvZmZzZXQ9IjEiIHN0eWxlPSJzdG9wLWNvbG9yOiNGRkZGRkY7c3RvcC1vcGFjaXR5OjAiLz4NCjwvcmFkaWFsR3JhZGllbnQ+DQo8cGF0aCBmaWxsPSJ1cmwoI1NWR0lEXzJfKSIgZD0iTTI2Mi4wNDQtNDIuMjM5QzM3MC4wOTctMTQuNjQ0LDQ1MCw4My4zNDQsNDUwLDE5OS45OThjMCwyLjY2MS0wLjA1Nyw1LjMxMS0wLjE0Myw3Ljk1MQ0KCWwtMTY4LjE1MS0yMy45MTZjLTQuOTIyLTI1LjMzOC0yMS4zMzMtNDYuNTU2LTQzLjU5NC01OC4wNDVMMjYyLjA0NC00Mi4yMzl6IE0xMjUuOTksMTYxLjg4DQoJYzExLjQ4OC0yMi4yNjEsMzIuNzA3LTM4LjY3LDU4LjA0My00My41OTNsMjMuOTItMTY4LjE1M2MtMi42NDMtMC4wODMtNS4yOS0wLjEzOS03Ljk1My0wLjEzOQ0KCWMtMTE2LjY1MiwwLTIxNC42MzksNzkuODk4LTI0Mi4yMzUsMTg3Ljk1M0wxMjUuOTksMTYxLjg4eiBNMTYxLjg4MywyNzQuMDA4Yy0yMi4yNTktMTEuNDg5LTM4LjY3LTMyLjcxLTQzLjU5Mi01OC4wNDUNCglsLTE2OC4xNTItMjMuOTJjLTAuMDgzLDIuNjQzLTAuMTM5LDUuMjkzLTAuMTM5LDcuOTU1YzAsMTE2LjY0OCw3OS44OTgsMjE0LjYzNywxODcuOTUyLDI0Mi4yMzNMMTYxLjg4MywyNzQuMDA4eiBNMjc0LjAxLDIzOC4xMTMNCgljLTExLjQ5LDIyLjI2MS0zMi43MDcsMzguNjY5LTU4LjA0Niw0My41OTNsLTIzLjkxOSwxNjguMTU4YzIuNjQzLDAuMDgzLDUuMjkyLDAuMTQsNy45NTQsMC4xNA0KCWMxMTYuNjUzLDAsMjE0LjY0LTc5LjkwMSwyNDIuMjMyLTE4Ny45NTVMMjc0LjAxLDIzOC4xMTN6Ii8+DQo8L3N2Zz4NCg=='
tags:
- source
- steamcmd

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('allocations', function (Blueprint $table) {
$table->boolean('is_locked')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('allocations', function (Blueprint $table) {
$table->dropColumn('is_locked');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('eggs', function (Blueprint $table) {
$table->longText('image')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('eggs', function (Blueprint $table) {
$table->dropColumn('image');
});
}
};

View File

@@ -21,6 +21,7 @@ return [
],
'user' => [
'account' => [
'username-changed' => 'Changed username from <b>:old</b> to <b>:new</b>',
'email-changed' => 'Changed email from <b>:old</b> to <b>:new</b>',
'password-changed' => 'Changed password',
],

View File

@@ -17,6 +17,7 @@ return [
'intro-update-available' => [
'heading' => 'Update available',
'content' => ':latestVersion is now available! Read our documentation to update your Panel.',
'button_changelog' => 'What\'s New?',
],
'intro-no-update' => [
'heading' => 'Your Panel is up to date',

View File

@@ -13,6 +13,9 @@ return [
'import' => [
'file' => 'File',
'url' => 'URL',
'image_url' => 'Image URL',
'image_error' => 'Could not fetch image',
'image_too_large' => 'Image too large. Limit is 1024KB',
'egg_help' => 'This should be the raw .json/.yaml file',
'url_help' => 'URLs must point directly to the raw .json/.yaml file',
'add_url' => 'New URL',
@@ -20,6 +23,13 @@ return [
'import_success' => 'Import Success',
'github' => 'Add from Github',
'refresh' => 'Refresh',
'import_image' => 'Import Image',
'no_local_ip' => 'Local IP Addresses are not allowed',
'unsupported_format' => 'Unsupported Format. Supported Formats: :formats',
'invalid_url' => 'The provided URL is invalid',
'image_deleted' => 'Image Deleted',
'no_image' => 'No Image Provided',
'image_updated' => 'Image Updated',
],
'export' => [
'modal' => 'How would you like to export :egg ?',

26
lang/en/admin/log.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
return [
'empty_table' => 'Yay! No Errors!',
'total_logs' => 'Total Logs',
'error' => 'Error',
'warning' => 'Warning',
'notice' => 'Notice',
'info' => 'Info',
'debug' => 'Debug',
'navigation' => [
'panel_logs' => 'Panel Logs',
],
'actions' => [
'upload_logs' => 'Upload Logs?',
'upload_logs_description' => 'This will upload :file to :url Are you sure you wish to do this?',
'view_logs' => 'View Logs',
'log_not_found' => 'Log not found!',
'log_not_found_description' => 'Could not find log for :filename',
'failed_to_upload' => 'Failed to upload.',
'failed_to_upload_description' => 'HTTP Status: :status',
'log_upload' => 'Log Uploaded!',
'log_upload_action' => 'View Log',
'upload_tooltip' => 'Upload to :url',
],
];

View File

@@ -10,6 +10,7 @@ return [
'basic_settings' => 'Basic Settings',
'advanced_settings' => 'Advanced Settings',
'config_file' => 'Configuration File',
'diagnostics' => 'Diagnostics',
],
'table' => [
'health' => 'Health',
@@ -43,7 +44,7 @@ return [
'error' => 'This is the domain name that points to your node\'s IP Address. If you\'ve already set up this, you can verify it by checking the next field!',
'fqdn_help' => 'Your panel is currently secured via an SSL certificate and that means your nodes require one too. You must use a domain name, because you cannot get SSL certificates for IP Addresses.',
'dns' => 'DNS Record Check',
'dns_help' => 'This lets you know if you DNS record is pointing to the correct IP address.',
'dns_help' => 'This lets you know if your DNS record is pointing to the correct IP address.',
'valid' => 'Valid',
'invalid' => 'Invalid',
'port' => 'Port',
@@ -117,8 +118,35 @@ return [
'error_connecting_description' => 'The configuration could not be automatically updated on Wings, you will need to manually update the configuration file.',
'allocation' => 'Allocation',
'diagnostics' => [
'header' => 'Node Diagnostics',
'include_endpoints' => 'Include Endpoints',
'include_endpoints_hint' => 'Including endpoints will show panel urls within the logs and NOT obscure them.',
'include_logs' => 'Include Logs',
'include_logs_hint' => 'Including logs will show recent logs and help track down possible issues.',
'run_diagnostics' => 'Run Diagnostics',
'upload_to_pelican' => 'Upload Logs',
'logs_pulled' => 'Logs Pulled!',
'logs_uploaded' => 'Logs Uploaded',
'upload_failed' => 'Logs Upload Failed',
'view_logs' => 'View Logs',
'pull' => 'Pull',
'upload' => 'Upload',
'clear' => 'Clear',
'404' => 'The requested diagnostic report could not be found. Make sure wings is up to date and try again.',
],
'cloudflare_issue' => [
'title' => 'Cloudflare Issue',
'body' => 'Your Node is not accessible by Cloudflare',
],
'bulk_update_ip' => 'Update IPs',
'bulk_update_ip_description' => 'Replace an old IP address with a new one for allocations. This is useful when a node\'s IP address changes',
'update_ip' => 'Update IP',
'old_ip' => 'Old IP Address',
'new_ip' => 'New IP Address',
'no_allocations_to_update' => 'No allocations with the selected old IP address were found',
'ip_updated' => 'Successfully updated :count of :total allocation(s)',
'ip_update_failed' => ':count allocation(s) failed to update',
];

View File

@@ -13,6 +13,10 @@ return [
'ports' => 'Ports',
'alias' => 'Alias',
'alias_helper' => 'Optional display name to help you remember what these are.',
'locked' => 'Locked?',
'locked_helper' => 'Users won\'t be able to delete locked allocations',
'lock' => 'Lock',
'unlock' => 'Unlock',
'name' => 'Name',
'external_id' => 'External ID',
'owner' => 'Owner',

View File

@@ -20,8 +20,10 @@ return [
'app_favicon_help' => 'Favicon should be placed in the public folder located in the root panel directory.',
'debug_mode' => 'Debug Mode',
'navigation' => 'Navigation',
'default_navigation' => 'Default Navigation Type',
'sidebar' => 'Sidebar',
'topbar' => 'Topbar',
'mixed' => 'Mixed',
'unit_prefix' => 'Unit Prefix',
'decimal_prefix' => 'Decimal Prefix (MB/GB)',
'binary_prefix' => 'Binary Prefix (MiB/GiB)',

View File

@@ -6,7 +6,6 @@ return [
'model_label_plural' => 'Webhooks',
'endpoint' => 'Endpoint',
'description' => 'Description',
'events' => 'Events',
'no_webhooks' => 'No Webhooks',
'help' => 'Help',
'help_text' => 'You have to wrap variable name in between {{ }} for example if you want to get the name from the api you can use {{name}}.',
@@ -38,7 +37,6 @@ return [
'thumbnail' => 'Thumbnail URL',
'embeds' => 'Embeds',
'thread_name' => 'Forum Thread Name',
'flags' => 'Flags',
'allowed_mentions' => 'Allowed Mentions',
'roles' => 'Roles',
'users' => 'Users',

View File

@@ -61,8 +61,9 @@ return [
'graph_period' => 'Graph Period',
'graph_period_helper' => 'The amount of data points, seconds, shown on the console graphs.',
'navigation' => 'Navigation Type',
'top' => 'Topbar',
'side' => 'Sidebar',
'sidebar' => 'Sidebar',
'topbar' => 'Topbar',
'mixed' => 'Mixed',
'no_oauth' => 'No Accounts Linked',
'no_api_keys' => 'No API Keys',
'no_ssh_keys' => 'No SSH Keys',

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