Compare commits

...

27 Commits

Author SHA1 Message Date
github-actions[bot]
d39241839c ci(release): bump version 2025-09-04 21:31:53 +00:00
Charles
925ab26fb4 Encode file path in url for folders (#1662) 2025-09-04 17:24:58 -04:00
Charles
2952e22619 Encode file path in url (#1661) 2025-09-04 17:15:46 -04:00
MartinOscar
079eaed010 Fix finish & add translation for Installer title (#1659) 2025-09-04 21:39:10 +02:00
MartinOscar
6671d45651 Fix various Translations & add Installer & add Notifications (#1632) 2025-09-04 20:17:59 +02:00
Boy132
3543b4773a Rename api key prefixes for better clarity (#1650) 2025-09-04 08:43:06 +02:00
IThundxr
02f788a659 Fix auto deploy docker command not including the container argument (#1584)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-09-03 22:30:18 +02:00
Boy132
7ace3978d8 Remove leftovers from activity log batch (#1649) 2025-09-03 22:26:17 +02:00
Boy132
8f277aaca0 Create custom startup variable field (#1615) 2025-09-02 09:05:36 +02:00
SaurFort
76451fa0ad fix: Wrong conversion if decimal prefix selected (#1626)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-08-31 13:51:27 +02:00
Boy132
0104a08ba4 Create custom number format method to catch invalid languages on php 8.4 (#1623) 2025-08-31 13:48:47 +02:00
MartinOscar
5eff006843 Fix activityLog permission name (#1641) 2025-08-31 12:59:48 +02:00
MartinOscar
a8241bf9f3 Fix Installer, Admin & Exit admin redirect (#1640) 2025-08-30 14:37:59 +02:00
MartinOscar
4aae2562ea Update bug-report logs url (#1630) 2025-08-25 12:13:27 +02:00
Boy132
42db5b328a Fix translation for invalid schedule cron + cleanup translations for import modal (#1618) 2025-08-18 23:54:25 +02:00
Boy132
bc4dfb3e92 Fix 500 for closeable alert banners (#1620) 2025-08-18 23:53:59 +02:00
Michael (Parker) Parker
3b9c81534f fix php ini permissions (#1619) 2025-08-17 09:34:41 -05:00
Boy132
f31aa78f6f Fix gap for profile repeaters (api keys, ssh keys, activity logs) (#1613) 2025-08-15 14:07:23 +02:00
Boy132
b5ebd544f4 Improve translation for "link" and "unlink" (oauth) (#1612) 2025-08-15 14:06:53 +02:00
Boy132
c77a37ec89 Fix & cleanup OAuthController (#1599) 2025-08-14 08:29:58 +02:00
Michael (Parker) Parker
4d78e5dcd1 Merge pull request #1609 from parkervcp/add_fcgi_healthcheck
add missing package for healthcheck
2025-08-13 14:15:44 -05:00
Michael (Parker) Parker
15075b6ab8 re-add file server directive 2025-08-13 13:44:21 -05:00
Lance Pioch
a8f233e204 Laravel 12.23.1 Shift (#1604)
Co-authored-by: Shift <shift@laravelshift.com>
2025-08-13 08:01:48 -04:00
Boy132
795cad43b9 Server creation: Only get node_id from allocation if it is missing (#1598) 2025-08-12 15:02:49 -04:00
Charles
46934d7a85 fix eggs with [] (#1596) 2025-08-12 15:02:41 -04:00
Michael (Parker) Parker
06067f375c Add fcgi package for healthcheck
I missed adding the package to the dockerfile so the healthcheck is failing
2025-08-12 09:08:10 -05:00
Charles
d1df53c683 fix lang (#1590) 2025-08-11 18:12:33 -04:00
106 changed files with 1176 additions and 611 deletions

View File

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

View File

@@ -64,7 +64,7 @@ WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic
caddy ca-certificates supervisor supercronic fcgi
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@@ -85,7 +85,8 @@ RUN chown root:www-data ./ \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf

View File

@@ -68,7 +68,7 @@ WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic coreutils
caddy ca-certificates supervisor supercronic fcgi coreutils
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@@ -89,7 +89,8 @@ RUN chown root:www-data ./ \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string
{
return trans('server/console.status.' . strtolower($this->value));
return trans('server/console.status.' . $this->value);
}
public function isOffline(): bool

View File

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

View File

@@ -34,4 +34,6 @@ interface OAuthSchemaInterface
public function isEnabled(): bool;
public function shouldCreateMissingUsers(): bool;
public function shouldLinkMissingUsers(): bool;
}

View File

@@ -63,9 +63,20 @@ abstract class OAuthSchema implements OAuthSchemaInterface
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state): bool => (bool) $state)
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")),
Toggle::make("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")
->label(trans('admin/setting.oauth.link_missing_users'))
->columnSpanFull()
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")),
];
}
@@ -116,4 +127,11 @@ abstract class OAuthSchema implements OAuthSchemaInterface
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
}
}

View File

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

View File

@@ -119,7 +119,7 @@ class Settings extends Page implements HasForms
->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('OAuth')
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth')
->schema($this->oauthSettings()),
@@ -553,7 +553,7 @@ class Settings extends Page implements HasForms
->label(trans('admin/setting.oauth.enable'))
->color('success')
->steps($schema->getSetupSteps())
->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $schema->getName())
->modalHeading(trans('admin/setting.oauth.enable_schema', ['schema' => $schema->getName()]))
->modalSubmitActionLabel(trans('admin/setting.oauth.enable'))
->modalCancelAction(false)
->action(function ($data, Set $set) use ($key) {

View File

@@ -96,7 +96,7 @@ class ApiKeyResource extends Resource
])
->emptyStateIcon('tabler-key')
->emptyStateDescription('')
->emptyStateHeading(trans('admin/apikey.empty_table'))
->emptyStateHeading(trans('admin/apikey.empty'))
->emptyStateActions([
CreateAction::make(),
]);

View File

@@ -57,7 +57,8 @@ class CreateEgg extends CreateRecord
return $form
->schema([
Tabs::make()->tabs([
Tab::make(trans('admin/egg.tabs.configuration'))
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
TextInput::make('name')
@@ -123,7 +124,8 @@ class CreateEgg extends CreateRecord
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make(trans('admin/egg.tabs.process_management'))
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
@@ -146,7 +148,8 @@ class CreateEgg extends CreateRecord
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make(trans('admin/egg.tabs.egg_variables'))
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
@@ -239,7 +242,8 @@ class CreateEgg extends CreateRecord
]),
]),
]),
Tab::make(trans('admin/egg.tabs.install_script'))
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')

View File

@@ -44,7 +44,8 @@ class EditEgg extends EditRecord
return $form
->schema([
Tabs::make()->tabs([
Tab::make(trans('admin/egg.tabs.configuration'))
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->icon('tabler-egg')
->schema([
@@ -115,7 +116,8 @@ class EditEgg extends EditRecord
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make(trans('admin/egg.tabs.process_management'))
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon('tabler-server-cog')
->schema([
@@ -135,7 +137,8 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make(trans('admin/egg.tabs.egg_variables'))
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon('tabler-variable')
->schema([
@@ -228,7 +231,8 @@ class EditEgg extends EditRecord
]),
]),
]),
Tab::make(trans('admin/egg.tabs.install_script'))
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon('tabler-file-download')
->schema([

View File

@@ -22,7 +22,7 @@ class ServersRelationManager extends RelationManager
->heading(trans('admin/egg.servers'))
->columns([
TextColumn::make('user.username')
->label('Owner')
->label(trans('admin/server.owner'))
->icon('tabler-user')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->sortable(),

View File

@@ -404,7 +404,7 @@ class CreateNode extends CreateRecord
type="submit"
size="sm"
>
Create Node
{{ trans('admin/node.create') }}
</x-filament::button>
BLADE))),
]);

View File

@@ -64,7 +64,7 @@ class EditNode extends EditRecord
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tab::make('')
Tab::make('overview')
->label(trans('admin/node.tabs.overview'))
->icon('tabler-chart-area-line-filled')
->columns([
@@ -80,7 +80,7 @@ class EditNode extends EditRecord
->schema([
Placeholder::make('')
->label(trans('admin/node.wings_version'))
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' (' . trans('admin/node.latest') . ': ' . $versionService->latestWingsVersion() . ')'),
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])),
Placeholder::make('')
->label(trans('admin/node.cpu_threads'))
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
@@ -108,7 +108,8 @@ class EditNode extends EditRecord
View::make('filament.components.node-storage-chart')
->columnSpanFull(),
]),
Tab::make(trans('admin/node.tabs.basic_settings'))
Tab::make('basic_settings')
->label(trans('admin/node.tabs.basic_settings'))
->icon('tabler-server')
->schema([
TextInput::make('fqdn')
@@ -257,7 +258,7 @@ class EditNode extends EditRecord
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Tab::make('adv')
Tab::make('advanced_settings')
->label(trans('admin/node.tabs.advanced_settings'))
->columns([
'default' => 1,
@@ -525,7 +526,7 @@ class EditNode extends EditRecord
->suffix('%'),
]),
]),
Tab::make('Config')
Tab::make('config_file')
->label(trans('admin/node.tabs.config_file'))
->icon('tabler-code')
->schema([
@@ -553,7 +554,7 @@ class EditNode extends EditRecord
->modalFooterActionsAlignment(Alignment::Center)
->form([
ToggleButtons::make('docker')
->label('Type')
->label(trans('admin/node.auto_label'))
->live()
->helperText(trans('admin/node.auto_question'))
->inline()

View File

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

View File

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

View File

@@ -129,7 +129,6 @@ class RoleResource extends Resource
->required()
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
TextInput::make('guard_name')
->label('Guard Name')
->default(Role::DEFAULT_GUARD_NAME)
->nullable()
->hidden(),

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Components\Forms\Fields\StartupVariable;
use App\Models\Allocation;
use App\Models\Egg;
use App\Models\Node;
@@ -13,11 +14,9 @@ use App\Services\Servers\ServerCreationService;
use App\Services\Users\UserCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Closure;
use Exception;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Hidden;
@@ -41,7 +40,6 @@ use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
use LogicException;
@@ -266,7 +264,7 @@ class CreateServer extends CreateRecord
->preload()
->disableOptionsWhenSelectedInSiblingRepeaterItems()
->prefixIcon('tabler-network')
->label('Additional Allocations')
->label(trans('admin/server.additional_allocations'))
->columnSpan(2)
->disabled(fn (Get $get) => $get('../../allocation_id') === null || $get('../../node_id') === null)
->searchable(['ip', 'port', 'ip_alias'])
@@ -429,7 +427,7 @@ class CreateServer extends CreateRecord
),
Repeater::make('server_variables')
->label('')
->hiddenLabel()
->relationship('serverVariables', fn (Builder $query) => $query->orderByPowerJoins('variable.sort'))
->saveRelationshipsBeforeChildrenUsing(null)
->saveRelationshipsUsing(null)
@@ -439,51 +437,15 @@ class CreateServer extends CreateRecord
->deletable(false)
->default([])
->hidden(fn ($state) => empty($state))
->schema(function () {
$text = TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->dehydratedWhenHidden()
->required(fn (Get $get) => in_array('required', $get('rules')))
->rules(
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $get('rules'),
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'))->toString();
$fail($message);
}
},
);
$select = Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
->dehydratedWhenHidden()
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
$components = [$text, $select];
foreach ($components as &$component) {
$component = $component
->live(onBlur: true)
->hintIcon('tabler-code')
->label(fn (Get $get) => $get('name'))
->hintIconTooltip(fn (Get $get) => implode('|', $get('rules')))
->prefix(fn (Get $get) => '{{' . $get('env_variable') . '}}')
->helperText(fn (Get $get) => empty($get('description')) ? '—' : $get('description'))
->afterStateUpdated(function (Set $set, Get $get, $state) {
$environment = $get($envPath = '../../environment');
$environment[$get('env_variable')] = $state;
$set($envPath, $environment);
});
}
return $components;
})
->schema([
StartupVariable::make('variable_value')
->fromForm()
->afterStateUpdated(function (Set $set, Get $get, $state) {
$environment = $get($envPath = '../../environment');
$environment[$get('env_variable')] = $state;
$set($envPath, $environment);
}),
])
->columnSpan(2),
]),
]),
@@ -799,7 +761,7 @@ class CreateServer extends CreateRecord
KeyValue::make('docker_labels')
->live()
->label('Container Labels')
->label(trans('admin/server.container_labels'))
->keyLabel(trans('admin/server.title'))
->valueLabel(trans('admin/server.description'))
->columnSpanFull(),
@@ -815,7 +777,7 @@ class CreateServer extends CreateRecord
type="submit"
size="sm"
>
Create Server
{{ trans('admin/server.create') }}
</x-filament::button>
BLADE))),
]);
@@ -851,40 +813,6 @@ class CreateServer extends CreateRecord
}
}
private function shouldHideComponent(Get $get, Component $component): bool
{
$containsRuleIn = collect($get('rules'))->reduce(
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
);
if ($component instanceof Select) {
return $containsRuleIn;
}
if ($component instanceof TextInput) {
return !$containsRuleIn;
}
throw new Exception('Component type not supported: ' . $component::class);
}
/**
* @return array<array-key, string>
*/
private function getSelectOptionsFromRules(Get $get): array
{
$inRule = collect($get('rules'))->reduce(
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
);
return str($inRule)
->after('in:')
->explode(',')
->each(fn ($value) => str($value)->trim())
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
/**
* @param string[] $portEntries
* @return array<int>

View File

@@ -7,6 +7,7 @@ use App\Enums\SuspendAction;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Components\Forms\Actions\PreviewStartupAction;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Forms\Fields\StartupVariable;
use App\Filament\Server\Pages\Console;
use App\Models\Allocation;
use App\Models\Database;
@@ -27,10 +28,8 @@ use App\Services\Servers\ToggleInstallService;
use App\Services\Servers\TransferServerService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Closure;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Actions as FormActions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Component;
@@ -56,7 +55,6 @@ use Filament\Support\Enums\Alignment;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
use LogicException;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
@@ -89,7 +87,8 @@ class EditServer extends EditRecord
])
->columnSpanFull()
->tabs([
Tab::make(trans('admin/server.tabs.information'))
Tab::make('information')
->label(trans('admin/server.tabs.information'))
->icon('tabler-info-circle')
->schema([
TextInput::make('name')
@@ -221,7 +220,8 @@ class EditServer extends EditRecord
])
->disabled(),
]),
Tab::make(trans('admin/server.tabs.environment_configuration'))
Tab::make('environment_configuration')
->label(trans('admin/server.tabs.environment_configuration'))
->icon('tabler-brand-docker')
->schema([
Fieldset::make(trans('admin/server.resource_limits'))
@@ -528,7 +528,8 @@ class EditServer extends EditRecord
->columnSpanFull(),
]),
]),
Tab::make(trans('admin/server.egg'))
Tab::make('egg')
->label(trans('admin/server.egg'))
->icon('tabler-egg')
->columns([
'default' => 1,
@@ -617,7 +618,7 @@ class EditServer extends EditRecord
}),
Repeater::make('server_variables')
->label('')
->hiddenLabel()
->relationship('serverVariables', function (Builder $query) {
/** @var Server $server */
$server = $this->getRecord();
@@ -634,64 +635,26 @@ class EditServer extends EditRecord
return $query->orderByPowerJoins('variable.sort');
})
->grid()
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
foreach ($data as $key => $value) {
if (!isset($data['variable_value'])) {
$data['variable_value'] = '';
}
}
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['variable_value'] ??= '';
return $data;
})
->reorderable(false)->addable(false)->deletable(false)
->schema(function () {
$text = TextInput::make('variable_value')
->hidden($this->shouldHideComponent(...))
->dehydratedWhenHidden()
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
->rules([
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $serverVariable->variable->rules,
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
$fail($message);
}
},
]);
$select = Select::make('variable_value')
->hidden($this->shouldHideComponent(...))
->dehydratedWhenHidden()
->options($this->getSelectOptionsFromRules(...))
->selectablePlaceholder(false);
$components = [$text, $select];
foreach ($components as &$component) {
$component = $component
->live(onBlur: true)
->hintIcon('tabler-code')
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
}
return $components;
})
->schema([
StartupVariable::make('variable_value')
->fromRecord(),
])
->columnSpan(6),
]),
Tab::make(trans('admin/server.mounts'))
Tab::make('mounts')
->label(trans('admin/server.mounts'))
->icon('tabler-layers-linked')
->schema(fn (Get $get) => [
ServerResource::getMountCheckboxList($get),
]),
Tab::make(trans('admin/server.databases'))
Tab::make('databases')
->label(trans('admin/server.databases'))
->hidden(fn () => !auth()->user()->can('viewAny', Database::class))
->icon('tabler-database')
->columns(4)
@@ -821,7 +784,8 @@ class EditServer extends EditRecord
]),
])->alignCenter()->columnSpanFull(),
]),
Tab::make(trans('admin/server.actions'))
Tab::make('actions')
->label(trans('admin/server.actions'))
->icon('tabler-settings')
->schema([
Fieldset::make(trans('admin/server.actions'))
@@ -954,12 +918,12 @@ class EditServer extends EditRecord
$transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', []));
Notification::make()
->title('Transfer started')
->title(trans('admin/server.notifications.transfer_started'))
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Transfer failed')
->title(trans('admin/server.notifications.transfer_failed'))
->body($exception->getMessage())
->danger()
->send();
@@ -1145,34 +1109,4 @@ class EditServer extends EditRecord
{
return null;
}
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
{
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
if ($component instanceof Select) {
return !$containsRuleIn;
}
if ($component instanceof TextInput) {
return $containsRuleIn;
}
throw new Exception('Component type not supported: ' . $component::class);
}
/**
* @return array<string, string>
*/
private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
{
$inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'));
return str($inRule)
->after('in:')
->explode(',')
->each(fn ($value) => str($value)->trim())
->mapWithKeys(fn ($value) => [$value => $value])
->all();
}
}

View File

@@ -80,7 +80,7 @@ class UserResource extends Resource
->label(trans('admin/user.email'))
->icon('tabler-mail'),
IconColumn::make('use_totp')
->label('2FA')
->label(trans('profile.tabs.2fa'))
->visibleFrom('lg')
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
->boolean(),

View File

@@ -211,7 +211,7 @@ class WebhookResource extends Resource
TextInput::make('thread_name')
->label(trans('admin/webhook.discord_message.forum_thread')),
CheckboxList::make('flags')
->label('Flags')
->label(trans('admin/webhook.discord_embed.flags'))
->options([
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds'),
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications'),

View File

@@ -61,13 +61,13 @@ class ListServers extends ListRecords
{
return [
TextColumn::make('condition')
->label('Status')
->label(trans('server/dashboard.status'))
->badge()
->tooltip(fn (Server $server) => $server->formatResource(ServerResourceType::Uptime))
->icon(fn (Server $server) => $server->condition->getIcon())
->color(fn (Server $server) => $server->condition->getColor()),
TextColumn::make('name')
->label('Server')
->label(trans('server/dashboard.title'))
->description(fn (Server $server) => $server->description)
->grow()
->searchable(),
@@ -142,15 +142,18 @@ class ListServers extends ListRecords
$other = (clone $all)->whereNot('owner_id', auth()->user()->id);
return [
'my' => Tab::make('My Servers')
'my' => Tab::make('my')
->label(trans('server/dashboard.tabs.my'))
->badge(fn () => $my->count())
->modifyQueryUsing(fn () => $my),
'other' => Tab::make('Others\' Servers')
'other' => Tab::make('other')
->label(trans('server/dashboard.tabs.other'))
->badge(fn () => $other->count())
->modifyQueryUsing(fn () => $other),
'all' => Tab::make('All Servers')
'all' => Tab::make('all')
->label(trans('server/dashboard.tabs.all'))
->badge($all->count()),
];
}
@@ -204,8 +207,8 @@ class ListServers extends ListRecords
$this->daemonPowerRepository->setServer($server)->send($action);
Notification::make()
->title('Power Action')
->body($action . ' sent to ' . $server->name)
->title(trans('server/dashboard.power_actions'))
->body(trans('server/dashboard.power_action_sent', ['action' => $action, 'name' => $server->name]))
->success()
->send();

View File

@@ -37,13 +37,13 @@ class ExportEggAction extends Action
$this->modalFooterActions([
Action::make('json')
->label(trans('admin/egg.export.as') . ' .json')
->label(trans('admin/egg.export.as', ['format' => 'json']))
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id, EggFormat::JSON);
}, 'egg-' . $egg->getKebabName() . '.json'))
->close(),
Action::make('yaml')
->label(trans('admin/egg.export.as') . ' .yaml')
->label(trans('admin/egg.export.as', ['format' => 'yaml']))
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id, EggFormat::YAML);
}, 'egg-' . $egg->getKebabName() . '.yaml'))

View File

@@ -106,7 +106,8 @@ class ImportEggAction extends Action
Tabs::make('Tabs')
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.file'))
Tab::make('file')
->label(trans('admin/egg.import.file'))
->icon('tabler-file-upload')
->schema([
FileUpload::make('files')
@@ -118,7 +119,8 @@ class ImportEggAction extends Action
->storeFiles(false)
->multiple($isMultiple),
]),
Tab::make(trans('admin/egg.import.url'))
Tab::make('url')
->label(trans('admin/egg.import.url'))
->icon('tabler-world-upload')
->schema([
Select::make('github')

View File

@@ -39,26 +39,28 @@ class ImportScheduleAction extends Action
Tabs::make('Tabs')
->contained(false)
->tabs([
Tab::make(trans('admin/schedule.import.file'))
Tab::make('file')
->label(trans('server/schedule.import_action.file'))
->icon('tabler-file-upload')
->schema([
FileUpload::make('files')
->label(trans('admin/schedule.model_label'))
->hint(trans('admin/schedule.import.schedule_help'))
->hiddenLabel()
->hint(trans('server/schedule.import_action.schedule_help'))
->acceptedFileTypes(['application/json'])
->preserveFilenames()
->previewable(false)
->storeFiles(false)
->multiple(true),
]),
Tab::make(trans('admin/schedule.import.url'))
Tab::make('url')
->label(trans('server/schedule.import_action.url'))
->icon('tabler-world-upload')
->schema([
Repeater::make('urls')
->label('')
->hiddenLabel()
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/schedule-')->before('.json')->headline())
->hint(trans('admin/schedule.import.url_help'))
->addActionLabel(trans('admin/schedule.import.add_url'))
->hint(trans('server/schedule.import_action.url_help'))
->addActionLabel(trans('server/schedule.import_action.add_url'))
->grid(2)
->reorderable(false)
->addable(true)
@@ -66,10 +68,10 @@ class ImportScheduleAction extends Action
->schema([
TextInput::make('url')
->live()
->label(trans('admin/schedule.import.url'))
->label(trans('server/schedule.import_action.url'))
->url()
->endsWith('.json')
->validationAttribute(trans('admin/schedule.import.url')),
->validationAttribute(trans('server/schedule.import_action.url')),
]),
]),
]),
@@ -104,14 +106,14 @@ class ImportScheduleAction extends Action
if ($failed->count() > 0) {
Notification::make()
->title(trans('admin/schedule.import.import_failed'))
->title(trans('server/schedule.import_action.import_failed'))
->body($failed->join(', '))
->danger()
->send();
}
if ($success->count() > 0) {
Notification::make()
->title(trans('admin/schedule.import.import_success'))
->title(trans('server/schedule.import_action.import_success'))
->body($success->join(', '))
->success()
->send();

View File

@@ -0,0 +1,181 @@
<?php
namespace App\Filament\Components\Forms\Fields;
use App\Enums\StartupVariableType;
use App\Models\ServerVariable;
use Closure;
use Filament\Forms\Components\Concerns\HasAffixes;
use Filament\Forms\Components\Concerns\HasExtraInputAttributes;
use Filament\Forms\Components\Concerns\HasPlaceholder;
use Filament\Forms\Components\Field;
use Filament\Forms\Get;
use Filament\Support\Concerns\HasExtraAlpineAttributes;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class StartupVariable extends Field
{
use HasAffixes;
use HasExtraAlpineAttributes;
use HasExtraInputAttributes;
use HasPlaceholder;
/** @var view-string */
protected string $view = 'filament.components.startup-variable';
protected string|Closure|null $variableName = null;
protected string|Closure|null $variableDesc = null;
protected string|Closure|null $variableEnv = null;
protected string|Closure|null $variableDefault = null;
/** @var string[]|Closure|null */
protected array|Closure|null $variableRules = [];
protected function setUp(): void
{
parent::setUp();
$this->label(fn (StartupVariable $component) => $component->getVariableName());
$this->prefix(fn (StartupVariable $component) => '{{' . $component->getVariableEnv() . '}}');
$this->hintIcon('tabler-code');
$this->hintIconTooltip(fn (StartupVariable $component) => implode('|', $component->getVariableRules()));
$this->helperText(fn (StartupVariable $component) => !$component->getVariableDesc() ? '—' : $component->getVariableDesc());
$this->rules(fn (StartupVariable $component) => $component->getVariableRules());
$this->placeholder(fn (StartupVariable $component) => $component->getVariableDefault());
$this->live(onBlur: true);
}
public function fromForm(): static
{
$this->variableName(fn (Get $get) => $get('name'));
$this->variableDesc(fn (Get $get) => $get('description'));
$this->variableEnv(fn (Get $get) => $get('env_variable'));
$this->variableDefault(fn (Get $get) => $get('default_value'));
$this->variableRules(fn (Get $get) => $get('rules'));
return $this;
}
public function fromRecord(): static
{
$this->variableName(fn (ServerVariable $record) => $record->variable->name);
$this->variableDesc(fn (ServerVariable $record) => $record->variable->description);
$this->variableEnv(fn (ServerVariable $record) => $record->variable->env_variable);
$this->variableDefault(fn (ServerVariable $record) => $record->variable->default_value);
$this->variableRules(fn (ServerVariable $record) => $record->variable->rules);
return $this;
}
public function variableName(string|Closure|null $name): static
{
$this->variableName = $name;
return $this;
}
public function variableDesc(string|Closure|null $desc): static
{
$this->variableDesc = $desc;
return $this;
}
public function variableEnv(string|Closure|null $envVariable): static
{
$this->variableEnv = $envVariable;
return $this;
}
public function variableDefault(string|Closure|null $default): static
{
$this->variableDefault = $default;
return $this;
}
/** @param string[]|Closure|null $rules */
public function variableRules(array|Closure|null $rules): static
{
$this->variableRules = $rules;
return $this;
}
public function getVariableName(): ?string
{
return $this->evaluate($this->variableName);
}
public function getVariableDesc(): ?string
{
return $this->evaluate($this->variableDesc);
}
public function getVariableEnv(): ?string
{
return $this->evaluate($this->variableEnv);
}
public function getVariableDefault(): ?string
{
return $this->evaluate($this->variableDefault);
}
/** @return string[] */
public function getVariableRules(): array
{
return (array) ($this->evaluate($this->variableRules) ?? []);
}
public function isRequired(): bool
{
$rules = $this->getVariableRules();
return in_array('required', $rules);
}
public function getType(): StartupVariableType
{
$rules = $this->getVariableRules();
if (Arr::first($rules, fn ($value) => str($value)->startsWith('in:'), false)) {
return StartupVariableType::Select;
}
if (in_array('boolean', $rules)) {
return StartupVariableType::Toggle;
}
return StartupVariableType::Text;
}
/** @return string[] */
public function getSelectOptions(): array
{
$rules = $this->getVariableRules();
$inRule = Arr::first($rules, fn ($value) => str($value)->startsWith('in:'));
if ($inRule) {
return str($inRule)
->after('in:')
->explode(',')
->each(fn ($value) => Str::trim($value))
->all();
}
return [];
}
}

View File

@@ -39,13 +39,13 @@ class ExportEggAction extends Action
$this->modalFooterActions([
Action::make('json')
->label(trans('admin/egg.export.as') . ' .json')
->label(trans('admin/egg.export.as', ['format' => 'json']))
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id, EggFormat::JSON);
}, 'egg-' . $egg->getKebabName() . '.json'))
->close(),
Action::make('yaml')
->label(trans('admin/egg.export.as') . ' .yaml')
->label(trans('admin/egg.export.as', ['format' => 'yaml']))
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id, EggFormat::YAML);
}, 'egg-' . $egg->getKebabName() . '.yaml'))

View File

@@ -86,7 +86,8 @@ class ImportEggAction extends Action
Tabs::make('Tabs')
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.file'))
Tab::make('file')
->label(trans('admin/egg.import.file'))
->icon('tabler-file-upload')
->schema([
FileUpload::make('files')
@@ -98,7 +99,8 @@ class ImportEggAction extends Action
->storeFiles(false)
->multiple($isMultiple),
]),
Tab::make(trans('admin/egg.import.url'))
Tab::make('url')
->label(trans('admin/egg.import.url'))
->icon('tabler-world-upload')
->schema([
Select::make('github')

View File

@@ -86,7 +86,8 @@ class EditProfile extends BaseEditProfile
->schema([
Tabs::make()->persistTabInQueryString()
->schema([
Tab::make(trans('profile.tabs.account'))
Tab::make('account')
->label(trans('profile.tabs.account'))
->icon('tabler-user')
->schema([
TextInput::make('username')
@@ -160,7 +161,8 @@ class EditProfile extends BaseEditProfile
}),
]),
Tab::make(trans('profile.tabs.oauth'))
Tab::make('oauth')
->label(trans('profile.tabs.oauth'))
->icon('tabler-brand-oauth')
->visible(count($oauthSchemas) > 0)
->schema(function () use ($oauthSchemas) {
@@ -174,7 +176,7 @@ class EditProfile extends BaseEditProfile
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
$actions[] = Action::make("oauth_$id")
->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name)
->label(trans('profile.' . ($unlink ? 'unlink' : 'link'), ['name' => $name]))
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
->color(Color::hex($schema->getHexColor()))
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
@@ -199,7 +201,8 @@ class EditProfile extends BaseEditProfile
return [Actions::make($actions)];
}),
Tab::make(trans('profile.tabs.2fa'))
Tab::make('2fa')
->label(trans('profile.tabs.2fa'))
->icon('tabler-shield-lock')
->schema(function (TwoFactorSetupService $setupService) {
if ($this->getUser()->use_totp) {
@@ -263,7 +266,7 @@ class EditProfile extends BaseEditProfile
->content(fn () => new HtmlString("
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
"))
->helperText(trans('profile.setup_key') .': '. $secret),
->helperText(trans('profile.setup_key', ['secret' => $secret])),
TextInput::make('2facode')
->label(trans('profile.code'))
->requiredWith('2fapassword')
@@ -276,7 +279,8 @@ class EditProfile extends BaseEditProfile
];
}),
Tab::make(trans('profile.tabs.api_keys'))
Tab::make('api_keys')
->label(trans('profile.tabs.api_keys'))
->icon('tabler-key')
->schema([
Grid::make('name')->columns(5)->schema([
@@ -322,6 +326,7 @@ class EditProfile extends BaseEditProfile
Section::make(trans('profile.api_keys'))->columnSpan(2)->schema([
Repeater::make('api_keys')
->hiddenLabel()
->inlineLabel(false)
->relationship('apiKeys')
->addable(false)
->itemLabel(fn ($state) => $state['identifier'])
@@ -357,7 +362,8 @@ class EditProfile extends BaseEditProfile
]),
]),
Tab::make(trans('profile.tabs.ssh_keys'))
Tab::make('ssh_keys')
->label(trans('profile.tabs.ssh_keys'))
->icon('tabler-lock-code')
->schema([
Grid::make('name')->columns(5)->schema([
@@ -406,6 +412,7 @@ class EditProfile extends BaseEditProfile
Section::make(trans('profile.ssh_keys'))->columnSpan(2)->schema([
Repeater::make('ssh_keys')
->hiddenLabel()
->inlineLabel(false)
->relationship('sshKeys')
->addable(false)
->itemLabel(fn ($state) => $state['name'])
@@ -441,22 +448,27 @@ class EditProfile extends BaseEditProfile
]),
]),
Tab::make(trans('profile.tabs.activity'))
Tab::make('activity')
->label(trans('profile.tabs.activity'))
->icon('tabler-history')
->schema([
Repeater::make('activity')
->label('')
->hiddenLabel()
->inlineLabel(false)
->deletable(false)
->addable(false)
->relationship(null, function (Builder $query) {
$query->orderBy('timestamp', 'desc');
})
->schema([
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
Placeholder::make('log')
->hiddenLabel()
->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
]),
]),
Tab::make(trans('profile.tabs.customization'))
Tab::make('customization')
->label(trans('profile.tabs.customization'))
->icon('tabler-adjustments')
->schema([
Section::make(trans('profile.dashboard'))

View File

@@ -163,7 +163,7 @@ class Login extends BaseLogin
protected function getLoginFormComponent(): Component
{
return TextInput::make('login')
->label('Login')
->label(trans('filament-panels::pages/auth/login.title'))
->required()
->autocomplete()
->autofocus()

View File

@@ -17,7 +17,6 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Support\Enums\Alignment;
use Illuminate\Support\Number;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class Settings extends ServerFormPage
@@ -104,7 +103,7 @@ class Settings extends ServerFormPage
->prefixIcon('tabler-cpu')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : Number::format($server->cpu, locale: auth()->user()->language) . '%'),
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : format_number($server->cpu) . '%'),
TextInput::make('memory')
->label('')
->prefix(trans('server/setting.server_info.limits.memory'))
@@ -125,21 +124,21 @@ class Settings extends ServerFormPage
->prefixIcon('tabler-file-zip')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups' : $server->backups->count() . ' ' .trans('server/setting.server_info.limits.of') . ' ' . $state),
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/backup.empty') : $server->backups->count() . ' ' .trans('server/setting.server_info.limits.of', ['max' => $state])),
TextInput::make('database_limit')
->label('')
->prefix(trans('server/setting.server_info.limits.databases'))
->prefixIcon('tabler-database')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases' : $server->databases->count() . ' ' . trans('server/setting.server_info.limits.of') . ' ' .$state),
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/database.empty') : $server->databases->count() . ' ' . trans('server/setting.server_info.limits.of', ['max' => $state])),
TextInput::make('allocation_limit')
->label('')
->prefix(trans('server/setting.server_info.limits.allocations'))
->prefixIcon('tabler-network')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.no_allocations') : $server->allocations->count() . ' ' .trans('server/setting.server_info.limits.of') . ' ' . $state),
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.no_allocations') : $server->allocations->count() . ' ' .trans('server/setting.server_info.limits.of', ['max' => $state])),
]),
]),
Section::make(trans('server/setting.node_info.title'))
@@ -258,13 +257,13 @@ class Settings extends ServerFormPage
}
Notification::make()
->title(trans('server/setting.notification_name'))
->title(trans('server/setting.server_info.notification_name'))
->body(fn () => $original . ' -> ' . $name)
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title(trans('server/setting.failed'))
->title(trans('server/setting.server_info.failed'))
->body($exception->getMessage())
->danger()
->send();
@@ -289,13 +288,13 @@ class Settings extends ServerFormPage
}
Notification::make()
->title(trans('server/setting.notification_description'))
->title(trans('server/setting.server_info.notification_description'))
->body(fn () => $original . ' -> ' . $description)
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title(trans('server/setting.failed'))
->title(trans('server/setting.server_info.failed'))
->body($exception->getMessage())
->danger()
->send();

View File

@@ -121,10 +121,10 @@ class BackupResource extends Resource
Action::make('rename')
->icon('tabler-pencil')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label('Rename')
->label(trans('server/backup.actions.rename.title'))
->form([
TextInput::make('name')
->label('Backup Name')
->label(trans('server/backup.actions.rename.new_name'))
->required()
->maxLength(255)
->default(fn (Backup $backup) => $backup->name),
@@ -143,8 +143,7 @@ class BackupResource extends Resource
}
Notification::make()
->title('Backup Renamed')
->body('The backup has been successfully renamed.')
->title(trans('server/backup.actions.rename.notification_success'))
->success()
->send();
})

View File

@@ -120,7 +120,7 @@ class DatabaseResource extends Resource
])
->actions([
ViewAction::make()
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
->modalHeading(fn (Database $database) => trans('server/database.viewing', ['database' => $database->database])),
DeleteAction::make()
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
]);

View File

@@ -134,8 +134,8 @@ class EditFiles extends Page
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
} catch (FileSizeTooLargeException) {
AlertBanner::make('file_too_large')
->title('<code>' . basename($this->path) . '</code> is too large!')
->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
->title(trans('server/file.alerts.file_too_large.title', ['name' => basename($this->path)]))
->body(trans('server/file.alerts.file_too_large.body', ['max' => convert_bytes_to_readable(config('panel.files.max_edit_size'))]))
->danger()
->closable()
->send();
@@ -143,7 +143,7 @@ class EditFiles extends Page
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
} catch (FileNotFoundException) {
AlertBanner::make('file_not_found')
->title('<code>' . basename($this->path) . '</code> not found!')
->title(trans('server/file.alerts.file_not_found.title', ['name' => basename($this->path)]))
->danger()
->closable()
->send();
@@ -151,7 +151,7 @@ class EditFiles extends Page
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
} catch (FileNotEditableException) {
AlertBanner::make('file_is_directory')
->title('<code>' . basename($this->path) . '</code> is a directory')
->title(trans('server/file.alerts.file_not_found.title', ['name' => basename($this->path)]))
->danger()
->closable()
->send();
@@ -179,8 +179,8 @@ class EditFiles extends Page
if (str($path)->endsWith('.pelicanignore')) {
AlertBanner::make('.pelicanignore_info')
->title('You\'re editing a <code>.pelicanignore</code> file!')
->body('Any files or directories listed in here will be excluded from backups. Wildcards are supported by using an asterisk (<code>*</code>).<br>You can negate a prior rule by prepending an exclamation point (<code>!</code>).')
->title(trans('server/file.alerts.pelicanignore.title'))
->body(trans('server/file.alerts.pelicanignore.body'))
->info()
->closable()
->send();

View File

@@ -107,14 +107,14 @@ class ListFiles extends ListRecords
])
->recordUrl(function (File $file) use ($server) {
if ($file->is_directory) {
return self::getUrl(['path' => join_paths($this->path, $file->name)]);
return self::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]);
}
if (!auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) {
return null;
}
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
return $file->canEdit() ? EditFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]) : null;
})
->actions([
Action::make('view')
@@ -122,12 +122,12 @@ class ListFiles extends ListRecords
->label(trans('server/file.actions.open'))
->icon('tabler-eye')
->visible(fn (File $file) => $file->is_directory)
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
->url(fn (File $file) => self::getUrl(['path' => encode_path(join_paths($this->path, $file->name))])),
EditAction::make('edit')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->icon('tabler-edit')
->visible(fn (File $file) => $file->canEdit())
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
->url(fn (File $file) => EditFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))])),
ActionGroup::make([
Action::make('rename')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
@@ -135,7 +135,7 @@ class ListFiles extends ListRecords
->icon('tabler-forms')
->form([
TextInput::make('name')
->label(trans('server/file.actions.rename.name'))
->label(trans('server/file.actions.rename.file_name'))
->default(fn (File $file) => $file->name)
->required(),
])
@@ -181,7 +181,7 @@ class ListFiles extends ListRecords
->label(trans('server/file.actions.download'))
->icon('tabler-download')
->visible(fn (File $file) => $file->is_file)
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
->url(fn (File $file) => DownloadFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]), true),
Action::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label(trans('server/file.actions.move.title'))
@@ -440,7 +440,7 @@ class ListFiles extends ListRecords
->log();
} catch (FileExistsException) {
AlertBanner::make('file_already_exists')
->title('<code>' . $path . '</code> already exists!')
->title(trans('server/file.alerts.file_already_exists.title', ['name' => $path]))
->danger()
->closable()
->send();
@@ -481,7 +481,7 @@ class ListFiles extends ListRecords
} catch (FileExistsException) {
$path = join_paths($this->path, $data['name']);
AlertBanner::make('folder_already_exists')
->title('<code>' . $path . '</code> already exists!')
->title(trans('server/file.alerts.file_already_exists.title', ['name' => $path]))
->danger()
->closable()
->send();
@@ -525,7 +525,8 @@ class ListFiles extends ListRecords
Tabs::make()
->contained(false)
->schema([
Tab::make(trans('server/file.actions.upload.from_files'))
Tab::make('from_files')
->label(trans('server/file.actions.upload.from_files'))
->live()
->schema([
FileUpload::make('files')
@@ -535,7 +536,8 @@ class ListFiles extends ListRecords
->maxSize((int) round($server->node->upload_size * (config('panel.use_binary_prefix') ? 1.048576 * 1024 : 1000)))
->multiple(),
]),
Tab::make(trans('server/file.actions.upload.url'))
Tab::make('url')
->label(trans('server/file.actions.upload.url'))
->live()
->disabled(fn (Get $get) => count($get('files')) > 0)
->schema([

View File

@@ -36,7 +36,7 @@ class SearchFiles extends ListRecords
return [
$resource::getUrl() => $resource::getBreadcrumb(),
self::getUrl(['searchTerm' => $this->searchTerm]) => trans('server/file.actions.global_search.search') . ' "' . $this->searchTerm . '"',
self::getUrl(['searchTerm' => $this->searchTerm]) => trans('server/file.actions.global_search.search_for_term', ['term' => ' "' . $this->searchTerm . '"']),
];
}

View File

@@ -115,7 +115,15 @@ class ScheduleResource extends Resource
->visibleOn('view'),
Section::make('Cron')
->label(trans('server/schedule.cron'))
->description(fn (Get $get) => new HtmlString(trans('server/schedule.cron_body') . '<br>' . trans('server/schedule.cron_timezone', ['timezone' => auth()->user()->timezone, 'next_run' => Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(auth()->user()->timezone)])))
->description(function (Get $get) {
try {
$nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(auth()->user()->timezone);
} catch (Exception) {
$nextRun = trans('server/schedule.invalid');
}
return new HtmlString(trans('server/schedule.cron_body') . '<br>' . trans('server/schedule.cron_timezone', ['timezone' => auth()->user()->timezone, 'next_run' => $nextRun]));
})
->schema([
Actions::make([
CronPresetAction::make('hourly')

View File

@@ -95,7 +95,8 @@ class UserResource extends Resource
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make(str($data['name'])->headline())
$tabs[] = Tab::make($data['name'])
->label(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/user.permissions.' . $data['name'] . '_desc'))

View File

@@ -53,7 +53,8 @@ class ListUsers extends ListRecords
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make(str($data['name'])->headline())
$tabs[] = Tab::make($data['name'])
->label(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/user.permissions.' . $data['name'] . '_desc'))

View File

@@ -133,8 +133,8 @@ class ServerConsole extends Widget
public function websocketError(): void
{
AlertBanner::make('websocket_error')
->title('Could not connect to websocket!')
->body('Check your browser console for more details.')
->title(trans('server/console.websocket_error.title'))
->body(trans('server/console.websocket_error.body'))
->danger()
->send();
}

View File

@@ -7,7 +7,6 @@ use Carbon\Carbon;
use Filament\Facades\Filament;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number;
class ServerCpuChart extends ChartWidget
{
@@ -31,7 +30,7 @@ class ServerCpuChart extends ChartWidget
$cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))
->slice(-$period)
->map(fn ($value, $key) => [
'cpu' => Number::format($value, maxPrecision: 2),
'cpu' => round($value, 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
])
->all();

View File

@@ -7,7 +7,6 @@ use Carbon\Carbon;
use Filament\Facades\Filament;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Number;
class ServerMemoryChart extends ChartWidget
{
@@ -31,7 +30,7 @@ class ServerMemoryChart extends ChartWidget
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))
->slice(-$period)
->map(fn ($value, $key) => [
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2),
'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2),
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
])
->all();

View File

@@ -8,7 +8,6 @@ use App\Models\Server;
use Carbon\CarbonInterface;
use Filament\Notifications\Notification;
use Filament\Widgets\StatsOverviewWidget;
use Illuminate\Support\Number;
use Livewire\Attributes\On;
class ServerOverview extends StatsOverviewWidget
@@ -54,9 +53,9 @@ class ServerOverview extends StatsOverviewWidget
}
$data = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))->last(default: 0);
$cpu = Number::format($data, maxPrecision: 2, locale: auth()->user()->language) . ' %';
$cpu = format_number($data, maxPrecision: 2) . ' %';
return $cpu . ($this->server->cpu > 0 ? ' / ' . Number::format($this->server->cpu, locale: auth()->user()->language) . ' %' : ' / ∞');
return $cpu . ($this->server->cpu > 0 ? ' / ' . format_number($this->server->cpu) . ' %' : ' / ∞');
}
public function memoryUsage(): string
@@ -68,7 +67,7 @@ class ServerOverview extends StatsOverviewWidget
}
$latestMemoryUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->last(default: 0);
$totalMemory = $this->server->memory * 2 ** 20;
$totalMemory = $this->server->memory * (config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000);
$used = convert_bytes_to_readable($latestMemoryUsed);
$total = convert_bytes_to_readable($totalMemory);
@@ -98,7 +97,7 @@ class ServerOverview extends StatsOverviewWidget
$this->js("window.navigator.clipboard.writeText('{$value}');");
Notification::make()
->title('Copied to clipboard')
->title(trans('server/dashboard.copied'))
->body($value)
->success()
->send();

View File

@@ -2,38 +2,37 @@
namespace App\Http\Controllers\Auth;
use App\Extensions\OAuth\OAuthSchemaInterface;
use App\Extensions\OAuth\OAuthService;
use App\Filament\Pages\Auth\EditProfile;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\Users\UserCreationService;
use App\Services\Users\UserUpdateService;
use Exception;
use Filament\Notifications\Notification;
use Illuminate\Auth\AuthManager;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Laravel\Socialite\Contracts\User as OAuthUser;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
class OAuthController extends Controller
{
public function __construct(
private readonly AuthManager $auth,
private UserCreationService $userCreation,
private readonly UserUpdateService $updateService,
private readonly UserCreationService $userCreation,
private readonly OAuthService $oauthService,
) {}
/**
* Redirect user to the OAuth provider
*/
public function redirect(string $driver): RedirectResponse
public function redirect(string $driver): SymfonyRedirectResponse|RedirectResponse
{
// Driver is disabled - redirect to normal login
if (!$this->oauthService->get($driver)->isEnabled()) {
return redirect()->route('auth.login');
}
return Socialite::with($driver)->redirect();
return Socialite::driver($driver)->redirect();
}
/**
@@ -43,7 +42,6 @@ class OAuthController extends Controller
{
$driver = $this->oauthService->get($driver);
// Unknown driver or driver is disabled - redirect to normal login
if (!$driver || !$driver->isEnabled()) {
return redirect()->route('auth.login');
}
@@ -52,67 +50,89 @@ class OAuthController extends Controller
if ($request->get('error')) {
report($request->get('error_description') ?? $request->get('error'));
Notification::make()
->title('Something went wrong')
->body($request->get('error'))
->danger()
->persistent()
->send();
return redirect()->route('auth.login');
return $this->errorRedirect($request->get('error'));
}
$oauthUser = Socialite::driver($driver->getId())->user();
// User is already logged in and wants to link a new OAuth Provider
if ($request->user()) {
$oauth = $request->user()->oauth;
$oauth[$driver->getId()] = $oauthUser->getId();
$this->updateService->handle($request->user(), ['oauth' => $oauth]);
$this->linkUser($request->user(), $driver, $oauthUser);
return redirect(EditProfile::getUrl(['tab' => '-oauth-tab'], panel: 'app'));
}
$user = User::whereJsonContains('oauth->'. $driver->getId(), $oauthUser->getId())->first();
if (!$user) {
// No user found and auto creation is disabled - redirect to normal login
if (!$driver->shouldCreateMissingUsers()) {
Notification::make()
->title('No linked User found')
->danger()
->persistent()
->send();
return redirect()->route('auth.login');
}
$username = $oauthUser->getNickname();
$email = $oauthUser->getEmail();
// Incomplete data, can't create user - redirect to normal login
if (!$email) {
Notification::make()
->title('No linked User found')
->danger()
->persistent()
->send();
return redirect()->route('auth.login');
}
$user = $this->userCreation->handle([
'username' => $username,
'email' => $email,
'oauth' => [
$driver->getId() => $oauthUser->getId(),
],
]);
if ($user) {
return $this->loginUser($user);
}
$this->auth->guard()->login($user, true);
return $this->handleMissingUser($driver, $oauthUser);
}
private function linkUser(User $user, OAuthSchemaInterface $driver, OAuthUser $oauthUser): User
{
$oauth = $user->oauth;
$oauth[$driver->getId()] = $oauthUser->getId();
$user->update(['oauth' => $oauth]);
return $user->refresh();
}
private function handleMissingUser(OAuthSchemaInterface $driver, OAuthUser $oauthUser): RedirectResponse
{
$email = $oauthUser->getEmail();
if (!$email) {
return $this->errorRedirect();
}
$user = User::whereEmail($email)->first();
if ($user) {
if (!$driver->shouldLinkMissingUsers()) {
return $this->errorRedirect();
}
$user = $this->linkUser($user, $driver, $oauthUser);
} else {
if (!$driver->shouldCreateMissingUsers()) {
return $this->errorRedirect();
}
try {
$user = $this->userCreation->handle([
'username' => $oauthUser->getNickname(),
'email' => $email,
'oauth' => [
$driver->getId() => $oauthUser->getId(),
],
]);
} catch (Exception $exception) {
report($exception);
return $this->errorRedirect();
}
}
return $this->loginUser($user);
}
private function loginUser(User $user): RedirectResponse
{
auth()->guard()->login($user, true);
return redirect('/');
}
private function errorRedirect(?string $error = null): RedirectResponse
{
Notification::make()
->title($error ? 'Something went wrong' : 'No linked User found')
->body($error)
->danger()
->persistent()
->send();
return redirect()->route('auth.login');
}
}

View File

@@ -16,12 +16,12 @@ class ServerInstalledListener
Notification::make()
->status($event->successful ? 'success' : 'danger')
->title('Server ' . ($event->initialInstall ? 'Installation' : 'Reinstallation') . ' ' . ($event->successful ? 'completed' : 'failed'))
->body('Server Name: ' . $event->server->name)
->title(trans('notifications.' . ($event->initialInstall ? 'installation' : 'reinstallation') . '_' . ($event->successful ? 'completed' : 'failed')))
->body(trans('server/setting.server_info.server_name', ['name' => $event->server->name]))
->actions([
Action::make('view')
->button()
->label('Open Server')
->label(trans('notifications.open_server'))
->markAsRead()
->url(fn () => Console::getUrl(panel: 'server', tenant: $event->server)),
])

View File

@@ -16,12 +16,12 @@ class SubUserAddedListener
$event->subuser->loadMissing('user');
Notification::make()
->title('Added to Server')
->body('You have been added as a subuser to ' . $event->subuser->server->name . '.')
->title(trans('notifications.user_added.title'))
->body(trans('notifications.user_added.body', ['server' => $event->subuser->server->name]))
->actions([
Action::make('view')
->button()
->label('Open Server')
->label(trans('notifications.open_server'))
->markAsRead()
->url(fn () => Console::getUrl(panel: 'server', tenant: $event->subuser->server)),
])

View File

@@ -11,8 +11,8 @@ class SubUserRemovedListener
public function handle(SubUserRemoved $event): void
{
Notification::make()
->title('Removed from Server')
->body('You have been removed as a subuser from ' . $event->server->name . '.')
->title(trans('notifications.user_removed.title'))
->body(trans('notifications.user_removed.body', ['server' => $event->server->name]))
->sendToDatabase($event->user);
$event->user->notify(new RemovedFromServer($event->server));

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Installer;
use App\Filament\Admin\Pages\Dashboard;
use App\Livewire\Installer\Steps\CacheStep;
use App\Livewire\Installer\Steps\DatabaseStep;
use App\Livewire\Installer\Steps\EnvironmentStep;
@@ -10,11 +9,16 @@ use App\Livewire\Installer\Steps\QueueStep;
use App\Livewire\Installer\Steps\RequirementsStep;
use App\Livewire\Installer\Steps\SessionStep;
use App\Models\User;
use App\Services\Helpers\LanguageService;
use App\Services\Users\UserCreationService;
use App\Traits\CheckMigrationsTrait;
use App\Traits\EnvironmentWriterTrait;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Wizard;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
@@ -23,6 +27,7 @@ use Filament\Notifications\Notification;
use Filament\Pages\SimplePage;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Exceptions\Halt;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
@@ -41,6 +46,11 @@ class PanelInstaller extends SimplePage implements HasForms
protected static string $view = 'filament.pages.installer';
public function getTitle(): string
{
return trans('installer.title');
}
public function getMaxWidth(): MaxWidth|string
{
return MaxWidth::SevenExtraLarge;
@@ -61,6 +71,10 @@ class PanelInstaller extends SimplePage implements HasForms
protected function getFormSchema(): array
{
return [
Grid::make()
->schema([
$this->getLanguageComponent(),
]),
Wizard::make([
RequirementsStep::make(),
EnvironmentStep::make($this),
@@ -70,20 +84,40 @@ class PanelInstaller extends SimplePage implements HasForms
SessionStep::make(),
])
->persistStepInQueryString()
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
->nextAction(function (Action $action) {
$action
->label(trans('installer.next_step'))
->keyBindings('enter');
})
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
type="submit"
size="sm"
wire:loading.attr="disabled"
>
Finish
{{ trans('installer.finish') }}
<span wire:loading><x-filament::loading-indicator class="h-4 w-4" /></span>
</x-filament::button>
BLADE))),
];
}
protected function getLanguageComponent(): Component
{
return Select::make('language')
->hiddenLabel()
->prefix(trans('profile.language'))
->prefixIcon('tabler-flag')
->required()
->live()
->default('en')
->selectablePlaceholder(false)
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->afterStateUpdated(fn ($state, Application $app) => $app->setLocale($state ?? config('app.locale')))
->native(false)
->columnStart(4);
}
protected function getFormStatePath(): ?string
{
return 'data';
@@ -106,7 +140,7 @@ class PanelInstaller extends SimplePage implements HasForms
$this->writeToEnv('env_session');
// Redirect to admin panel
$this->redirect(Dashboard::getUrl());
$this->redirect(Filament::getPanel('admin')->getUrl());
} catch (Halt) {
}
}
@@ -121,13 +155,13 @@ class PanelInstaller extends SimplePage implements HasForms
report($exception);
Notification::make()
->title('Could not write to .env file')
->title(trans('installer.exceptions.write_env'))
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while writing .env file');
throw new Halt(trans('installer.exceptions.write_env'));
}
Artisan::call('config:clear');
@@ -144,23 +178,23 @@ class PanelInstaller extends SimplePage implements HasForms
report($exception);
Notification::make()
->title('Migrations failed')
->title(trans('installer.database.exceptions.migration'))
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while running migrations');
throw new Halt(trans('installer.exceptions.migration'));
}
if (!$this->hasCompletedMigrations()) {
Notification::make()
->title('Migrations failed')
->title(trans('installer.database.exceptions.migration'))
->danger()
->persistent()
->send();
throw new Halt('Migrations failed');
throw new Halt(trans('installer.database.exceptions.migration'));
}
}
@@ -175,13 +209,13 @@ class PanelInstaller extends SimplePage implements HasForms
report($exception);
Notification::make()
->title('Could not create admin user')
->title(trans('installer.exceptions.create_user'))
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while creating admin user');
throw new Halt(trans('installer.exceptions.create_user'));
}
}
}

View File

@@ -24,13 +24,13 @@ class CacheStep
public static function make(PanelInstaller $installer): Step
{
return Step::make('cache')
->label('Cache')
->label(trans('installer.cache.title'))
->columns()
->schema([
ToggleButtons::make('env_cache.CACHE_STORE')
->label('Cache Driver')
->label(trans('installer.cache.driver'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
->hintIconTooltip(trans('installer.cache.driver_help'))
->required()
->inline()
->options(self::CACHE_DRIVERS)
@@ -50,31 +50,31 @@ class CacheStep
}
}),
TextInput::make('env_cache.REDIS_HOST')
->label('Redis Host')
->label(trans('installer.cache.fields.host'))
->placeholder('127.0.0.1')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
->hintIconTooltip(trans('installer.cache.fields.host_help'))
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.host') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_PORT')
->label('Redis Port')
->label(trans('installer.cache.fields.port'))
->placeholder('6379')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your redis server.')
->hintIconTooltip(trans('installer.cache.fields.port_help'))
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.port') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_USERNAME')
->label('Redis Username')
->label(trans('installer.cache.fields.username'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your redis user. Can be empty')
->hintIconTooltip(trans('installer.cache.fields.username_help'))
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.username') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_PASSWORD')
->label('Redis Password')
->label(trans('installer.cache.fields.password'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.')
->hintIconTooltip(trans('installer.cache.fields.password_help'))
->password()
->revealable()
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.password') : null)
@@ -110,7 +110,7 @@ class CacheStep
$redis->connection()->command('ping');
} catch (Exception $exception) {
Notification::make()
->title('Redis connection failed')
->title(trans('installer.cache.exception'))
->body($exception->getMessage())
->danger()
->send();

View File

@@ -25,13 +25,13 @@ class DatabaseStep
public static function make(PanelInstaller $installer): Step
{
return Step::make('database')
->label('Database')
->label(trans('installer.database.title'))
->columns()
->schema([
ToggleButtons::make('env_database.DB_CONNECTION')
->label('Database Driver')
->label(trans('installer.database.driver'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
->hintIconTooltip(trans('installer.database.driver_help'))
->required()
->inline()
->options(self::DATABASE_DRIVERS)
@@ -61,40 +61,40 @@ class DatabaseStep
}
}),
TextInput::make('env_database.DB_DATABASE')
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? trans('installer.database.fields.path') : trans('installer.database.fields.name'))
->placeholder(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')
->hintIcon('tabler-question-mark')
->hintIconTooltip(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
->hintIconTooltip(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? trans('installer.database.fields.path_help') : trans('installer.database.fields.name_help'))
->required()
->default('database.sqlite'),
TextInput::make('env_database.DB_HOST')
->label('Database Host')
->label(trans('installer.database.fields.host'))
->placeholder('127.0.0.1')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your database. Make sure it is reachable.')
->hintIconTooltip(trans('installer.database.fields.host_help'))
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_PORT')
->label('Database Port')
->label(trans('installer.database.fields.port'))
->placeholder('3306')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your database.')
->hintIconTooltip(trans('installer.database.fields.port_help'))
->numeric()
->minValue(1)
->maxValue(65535)
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_USERNAME')
->label('Database Username')
->label(trans('installer.database.fields.username'))
->placeholder('pelican')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your database user.')
->hintIconTooltip(trans('installer.database.fields.username_help'))
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_PASSWORD')
->label('Database Password')
->label(trans('installer.database.fields.password'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password of your database user. Can be empty.')
->hintIconTooltip(trans('installer.database.fields.password_help'))
->password()
->revealable()
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
@@ -103,7 +103,7 @@ class DatabaseStep
$driver = $get('env_database.DB_CONNECTION');
if (!self::testConnection($driver, $get('env_database.DB_HOST'), $get('env_database.DB_PORT'), $get('env_database.DB_DATABASE'), $get('env_database.DB_USERNAME'), $get('env_database.DB_PASSWORD'))) {
throw new Halt('Database connection failed');
throw new Halt(trans('installer.database.exceptions.connection'));
}
$installer->writeToEnv('env_database');
@@ -133,7 +133,7 @@ class DatabaseStep
DB::disconnect('_panel_install_test');
Notification::make()
->title('Database connection failed')
->title(trans('installer.database.exceptions.connection'))
->body($exception->getMessage())
->danger()
->send();

View File

@@ -12,36 +12,36 @@ class EnvironmentStep
public static function make(PanelInstaller $installer): Step
{
return Step::make('environment')
->label('Environment')
->label(trans('installer.environment.title'))
->columns()
->schema([
TextInput::make('env_general.APP_NAME')
->label('App Name')
->label(trans('installer.environment.fields.app_name'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the Name of your Panel.')
->hintIconTooltip(trans('installer.environment.fields.app_name_help'))
->required()
->default(config('app.name')),
TextInput::make('env_general.APP_URL')
->label('App URL')
->label(trans('installer.environment.fields.app_url'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the URL you access your Panel from.')
->hintIconTooltip(trans('installer.environment.fields.app_url_help'))
->required()
->default(url('')),
Fieldset::make('adminuser')
->label('Admin User')
Fieldset::make('admin_user')
->label(trans('installer.environment.fields.account.section'))
->columns(3)
->schema([
TextInput::make('user.email')
->label('E-Mail')
->label(trans('installer.environment.fields.account.email'))
->required()
->email()
->placeholder('admin@example.com'),
TextInput::make('user.username')
->label('Username')
->label(trans('installer.environment.fields.account.username'))
->required()
->placeholder('admin'),
TextInput::make('user.password')
->label('Password')
->label(trans('installer.environment.fields.account.password'))
->required()
->password()
->revealable(),

View File

@@ -22,35 +22,35 @@ class QueueStep
public static function make(PanelInstaller $installer): Step
{
return Step::make('queue')
->label('Queue')
->label(trans('installer.queue.title'))
->columns()
->schema([
ToggleButtons::make('env_queue.QUEUE_CONNECTION')
->label('Queue Driver')
->label(trans('installer.queue.driver'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->hintIconTooltip(trans('installer.queue.driver_help'))
->required()
->inline()
->options(self::QUEUE_DRIVERS)
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
->default(config('queue.default')),
Toggle::make('done')
->label('I have done both steps below.')
->label(trans('installer.queue.fields.done'))
->accepted(fn () => !@file_exists('/.dockerenv'))
->inline(false)
->validationMessages([
'accepted' => 'You need to do both steps before continuing!',
'accepted' => trans('installer.queue.fields.done_validation'),
])
->hidden(fn () => @file_exists('/.dockerenv')),
TextInput::make('crontab')
->label(new HtmlString('Run the following command to set up your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
->label(new HtmlString(trans('installer.queue.fields.crontab')))
->disabled()
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -')
->hidden(fn () => @file_exists('/.dockerenv'))
->columnSpanFull(),
TextInput::make('queueService')
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
->label(new HtmlString(trans('installer.queue.fields.service')))
->disabled()
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service')

View File

@@ -18,13 +18,13 @@ class RequirementsStep
$correctPhpVersion = $compare >= 0;
$fields = [
Section::make('PHP Version')
->description(self::MIN_PHP_VERSION . ' or newer')
Section::make(trans('installer.requirements.sections.version.title'))
->description(trans('installer.requirements.sections.version.or_newer', ['version' => self::MIN_PHP_VERSION]))
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
->iconColor($correctPhpVersion ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('Your PHP Version is ' . PHP_VERSION . '.'),
->content(trans('installer.requirements.sections.version.content', ['version' => PHP_VERSION])),
]),
];
@@ -41,16 +41,16 @@ class RequirementsStep
];
$allExtensionsInstalled = !in_array(false, $phpExtensions);
$fields[] = Section::make('PHP Extensions')
$fields[] = Section::make(trans('installer.requirements.sections.extensions.title'))
->description(implode(', ', array_keys($phpExtensions)))
->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x')
->iconColor($allExtensionsInstalled ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('All needed PHP Extensions are installed.')
->content(trans('installer.requirements.sections.extensions.good'))
->visible($allExtensionsInstalled),
Placeholder::make('')
->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false)))
->content(trans('installer.requirements.sections.extensions.bad', ['extensions' => implode(', ', array_keys($phpExtensions, false))]))
->visible(!$allExtensionsInstalled),
]);
@@ -60,30 +60,30 @@ class RequirementsStep
];
$correctFolderPermissions = !in_array(false, $folderPermissions);
$fields[] = Section::make('Folder Permissions')
$fields[] = Section::make(trans('installer.requirements.sections.permissions.title'))
->description(implode(', ', array_keys($folderPermissions)))
->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x')
->iconColor($correctFolderPermissions ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('All Folders have the correct permissions.')
->content(trans('installer.requirements.sections.permissions.good'))
->visible($correctFolderPermissions),
Placeholder::make('')
->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false)))
->content(trans('installer.requirements.sections.permissions.bad', ['folders' => implode(', ', array_keys($folderPermissions, false))]))
->visible(!$correctFolderPermissions),
]);
return Step::make('requirements')
->label('Server Requirements')
->label(trans('installer.requirements.title'))
->schema($fields)
->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) {
if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) {
Notification::make()
->title('Some requirements are missing!')
->title(trans('installer.requirements.exception'))
->danger()
->send();
throw new Halt('Some requirements are missing');
throw new Halt(trans('installer.requirements.title'));
}
});
}

View File

@@ -19,12 +19,12 @@ class SessionStep
public static function make(): Step
{
return Step::make('session')
->label('Session')
->label(trans('installer.session.title'))
->schema([
ToggleButtons::make('env_session.SESSION_DRIVER')
->label('Session Driver')
->label(trans('installer.session.driver'))
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
->hintIconTooltip(trans('installer.session.driver_help'))
->required()
->inline()
->options(self::SESSION_DRIVERS)

View File

@@ -38,7 +38,7 @@ class ServerEntry extends Component
<div class="flex justify-between text-center items-center gap-4">
<div>
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.cpu') }}</p>
<p class="text-md font-semibold">{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}</p>
<p class="text-md font-semibold">{{ format_number(0, precision: 2) . '%' }}</p>
<hr class="p-0.5">
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::CPULimit) }}</p>
</div>

View File

@@ -21,7 +21,6 @@ use Illuminate\Support\Str;
* \App\Models\ActivityLog.
*
* @property int $id
* @property string|null $batch
* @property string $event
* @property string $ip
* @property string|null $description
@@ -43,7 +42,6 @@ use Illuminate\Support\Str;
* @method static Builder|ActivityLog whereActorId($value)
* @method static Builder|ActivityLog whereActorType($value)
* @method static Builder|ActivityLog whereApiKeyId($value)
* @method static Builder|ActivityLog whereBatch($value)
* @method static Builder|ActivityLog whereDescription($value)
* @method static Builder|ActivityLog whereEvent($value)
* @method static Builder|ActivityLog whereId($value)
@@ -76,7 +74,6 @@ class ActivityLog extends Model implements HasIcon, HasLabel
/** @var array<array-key, string[]> */
public static array $validationRules = [
'event' => ['required', 'string'],
'batch' => ['nullable', 'uuid'],
'ip' => ['required', 'string'],
'description' => ['nullable', 'string'],
'properties' => ['array'],

View File

@@ -216,7 +216,7 @@ class ApiKey extends PersonalAccessToken
{
Assert::oneOf($type, [self::TYPE_ACCOUNT, self::TYPE_APPLICATION]);
return $type === self::TYPE_ACCOUNT ? 'plcn_' : 'peli_';
return $type === self::TYPE_ACCOUNT ? 'pacc_' : 'papp_';
}
/**

View File

@@ -195,7 +195,7 @@ class File extends Model
}
AlertBanner::make('files_node_error')
->title('Could not load files!')
->title(trans('server/file.alerts.files_node_error.title'))
->body($message->toString())
->danger()
->send();

View File

@@ -44,7 +44,7 @@ class Role extends BaseRole
'health' => [
'view',
],
'activity' => [
'activityLog' => [
'seeIps',
],
];

View File

@@ -17,7 +17,6 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Number;
use Psr\Http\Message\ResponseInterface;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -484,7 +483,7 @@ class Server extends Model implements Validatable
}
if ($resourceType->isPercentage()) {
return Number::format($resourceAmount, precision: 2, locale: auth()->user()->language ?? 'en') . '%';
return format_number($resourceAmount, precision: 2) . '%';
}
return convert_bytes_to_readable($resourceAmount, base: 3);

View File

@@ -3,7 +3,6 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Services\Activity\ActivityLogBatchService;
use App\Services\Activity\ActivityLogTargetableService;
class ActivityLogServiceProvider extends ServiceProvider
@@ -14,7 +13,6 @@ class ActivityLogServiceProvider extends ServiceProvider
*/
public function register(): void
{
$this->app->scoped(ActivityLogBatchService::class);
$this->app->scoped(ActivityLogTargetableService::class);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Providers\Filament;
use App\Filament\Pages\Auth\EditProfile;
use Filament\Facades\Filament;
use Filament\Navigation\MenuItem;
use Filament\Navigation\NavigationGroup;
use Filament\Panel;
@@ -23,7 +24,7 @@ class AdminPanelProvider extends PanelProvider
->url(fn () => EditProfile::getUrl(panel: 'app')),
MenuItem::make()
->label(fn () => trans('profile.exit_admin'))
->url('/')
->url(fn () => Filament::getPanel('app')->getUrl())
->icon('tabler-arrow-back')
->sort(24),
])

View File

@@ -18,7 +18,7 @@ class AppPanelProvider extends PanelProvider
->userMenuItems([
MenuItem::make()
->label(trans('profile.admin'))
->url('/admin')
->url(fn () => Filament::getPanel('admin')->getUrl())
->icon('tabler-arrow-forward')
->sort(5)
->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin'))),

View File

@@ -26,7 +26,7 @@ class ServerPanelProvider extends PanelProvider
->label(fn () => trans('filament-panels::pages/auth/edit-profile.label'))
->url(fn () => EditProfile::getUrl(panel: 'app')),
MenuItem::make()
->label('Server List')
->label(trans('server/dashboard.list'))
->icon('tabler-brand-docker')
->url(fn () => ListServers::getUrl(panel: 'app'))
->sort(6),

View File

@@ -31,8 +31,8 @@ class DaemonServerRepository extends DaemonRepository
if ($requestBadGateway && $requestFromCloudflare && !$requestCachedFromCloudflare) {
Notification::make()
->title('Cloudflare Issue')
->body('Your Node is not accessible by Cloudflare')
->title(trans('admin/node.cloudflare_issue.title'))
->body(trans('admin/node.cloudflare_issue.body'))
->danger()
->send();
}

View File

@@ -1,60 +0,0 @@
<?php
namespace App\Services\Activity;
use Ramsey\Uuid\Uuid;
class ActivityLogBatchService
{
protected int $transaction = 0;
protected ?string $uuid = null;
/**
* Returns the UUID of the batch, or null if there is not a batch currently
* being executed.
*/
public function uuid(): ?string
{
return $this->uuid;
}
/**
* Starts a new batch transaction. If there is already a transaction present
* this will be nested.
*/
public function start(): void
{
if ($this->transaction === 0) {
$this->uuid = Uuid::uuid4()->toString();
}
$this->transaction++;
}
/**
* Ends a batch transaction, if this is the last transaction in the stack
* the UUID will be cleared out.
*/
public function end(): void
{
$this->transaction = max(0, $this->transaction - 1);
if ($this->transaction === 0) {
$this->uuid = null;
}
}
/**
* Executes the logic provided within the callback in the scope of an activity
* log batch transaction.
*/
public function transaction(\Closure $callback): mixed
{
$this->start();
$result = $callback($this->uuid());
$this->end();
return $result;
}
}

View File

@@ -24,7 +24,6 @@ class ActivityLogService
public function __construct(
protected AuthFactory $manager,
protected ActivityLogBatchService $batch,
protected ActivityLogTargetableService $targetable,
protected ConnectionInterface $connection
) {}
@@ -201,7 +200,6 @@ class ActivityLogService
$this->activity = new ActivityLog([
'ip' => Request::ip(),
'batch_uuid' => $this->batch->uuid(),
'properties' => Collection::make([]),
'api_key_id' => $this->targetable->apiKeyId(),
]);

View File

@@ -20,7 +20,14 @@ class EggConfigurationService
* @return array{
* startup: array{done: string[], user_interaction: string[], strip_ansi: bool},
* stop: array{type: string, value: string},
* configs: array<mixed>
* configs: list<array{
* file: string,
* replace: list<array{
* match: string,
* if_value?: string,
* replace_with: string
* }>
* }>
* }
*/
public function handle(Server $server): array
@@ -81,9 +88,10 @@ class EggConfigurationService
}
/**
* @return array<mixed>
* @param array<string, mixed> $configs
* @return array<int, array<string, mixed>>
*/
protected function replacePlaceholders(Server $server, object $configs): array
protected function replacePlaceholders(Server $server, object|array $configs): array
{
// Get the legacy configuration structure for the server so that we
// can property map the egg placeholders to values.

View File

@@ -46,7 +46,7 @@ class VariableUpdateService
{
if (!is_null(array_get($data, 'env_variable'))) {
if (in_array(strtoupper(array_get($data, 'env_variable')), EggVariable::RESERVED_ENV_NAMES)) {
throw new ReservedVariableNameException(trans('exceptions.service.variables.reserved_name', ['name' => array_get($data, 'env_variable')]));
throw new ReservedVariableNameException(trans('exceptions.variables.reserved_name', ['name' => array_get($data, 'env_variable')]));
}
$search = EggVariable::query()
@@ -56,7 +56,7 @@ class VariableUpdateService
->count();
if ($search > 0) {
throw new DisplayException(trans('exceptions.service.variables.env_not_unique', ['name' => array_get($data, 'env_variable')]));
throw new DisplayException(trans('exceptions.variables.env_not_unique', ['name' => array_get($data, 'env_variable')]));
}
}

View File

@@ -49,7 +49,7 @@ class NodeAutoDeployService
return sprintf(
'%s wings configure --panel-url %s --token %s --node %d%s',
$docker ? 'docker compose exec -it' : 'sudo',
$docker ? 'docker compose exec -it $(docker ps --filter "name=wings" --format "{{.Names}}")' : 'sudo',
config('app.url'),
$token,
$node->id,

View File

@@ -56,8 +56,8 @@ class ServerCreationService
$egg = Egg::query()->findOrFail($data['egg_id']);
// Fill missing fields from egg
$data['image'] = $data['image'] ?? collect($egg->docker_images)->first();
$data['startup'] = $data['startup'] ?? $egg->startup;
$data['image'] ??= collect($egg->docker_images)->first();
$data['startup'] ??= $egg->startup;
// If a deployment object has been passed we need to get the allocation and node that the server should use.
if ($deployment) {
@@ -87,7 +87,7 @@ class ServerCreationService
$data['node_id'] = $nodes->first();
}
} else {
$data['node_id'] = Allocation::find($data['allocation_id'])?->node_id;
$data['node_id'] ??= Allocation::find($data['allocation_id'])?->node_id;
}
Assert::false(empty($data['node_id']), 'Expected a non-empty node_id in server creation data.');

View File

@@ -30,14 +30,14 @@ class SuspensionService
// suspended in the database. Additionally, nothing needs to happen if the server
// is not suspended, and we try to un-suspend the instance.
if ($isSuspending === $server->isSuspended()) {
Notification::make()->danger()->title('Failed!')->body('Server is already suspended!')->send();
Notification::make()->danger()->title(trans('notifications.failed'))->body(trans('admin/server.notifications.server_already_suspended'))->send();
return;
}
// Check if the server is currently being transferred.
if (!is_null($server->transfer)) {
Notification::make()->danger()->title('Failed!')->body('Server is currently being transferred.')->send();
Notification::make()->danger()->title(trans('notifications.failed'))->body(trans('admin/server.notifications.already_transfering'))->send();
throw new ConflictHttpException('Cannot toggle suspension status on a server that is currently being transferred.');
}

View File

@@ -26,7 +26,6 @@ class ActivityLogTransformer extends BaseClientTransformer
// the front-end for each entry to improve rendering performance since there
// is nothing else sufficiently unique to key off at this point.
'id' => sha1((string) $model->id),
'batch' => $model->batch,
'event' => $model->event,
'is_api' => !is_null($model->api_key_id),
'ip' => $this->canViewIP($model->actor) ? $model->ip : null,

View File

@@ -45,7 +45,7 @@ if (!function_exists('convert_bytes_to_readable')) {
$fromBase = log($bytes) / log($conversionUnit);
$base ??= floor($fromBase);
return Number::format(pow($conversionUnit, $fromBase - $base), $decimals, locale: auth()->user()->language) . ' ' . $suffix[$base];
return format_number(pow($conversionUnit, $fromBase - $base), precision: $decimals) . ' ' . $suffix[$base];
}
}
@@ -98,3 +98,22 @@ if (!function_exists('get_ip_from_hostname')) {
return false;
}
}
if (!function_exists('format_number')) {
function format_number(int|float $number, ?int $precision = null, ?int $maxPrecision = null): false|string
{
try {
return Number::format($number, $precision, $maxPrecision, auth()->user()->language ?? 'en');
} catch (Throwable) {
// User language is invalid, so default to english
return Number::format($number, $precision, $maxPrecision, 'en');
}
}
}
if (!function_exists('encode_path')) {
function encode_path(string $path): string
{
return implode('/', array_map('rawurlencode', explode('/', $path)));
}
}

View File

@@ -17,7 +17,7 @@
"doctrine/dbal": "~3.6.0",
"filament/filament": "^3.3",
"guzzlehttp/guzzle": "^7.9",
"laravel/framework": "^12.22",
"laravel/framework": "^12.23",
"laravel/helpers": "^1.7",
"laravel/sanctum": "^4.1",
"laravel/socialite": "^5.21",

246
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e5d9f294519edc6e4cca937c579709f8",
"content-hash": "9fe021f7367f1d07f7740ba2f5e02683",
"packages": [
{
"name": "abdelhamiderrahmouni/filament-monaco-editor",
@@ -1020,16 +1020,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.352.5",
"version": "3.352.7",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "e226dcc96c0a1165d9c8248ec637d1006b883609"
"reference": "26d8988376984e4684c497e71722a97b79aeef4e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e226dcc96c0a1165d9c8248ec637d1006b883609",
"reference": "e226dcc96c0a1165d9c8248ec637d1006b883609",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/26d8988376984e4684c497e71722a97b79aeef4e",
"reference": "26d8988376984e4684c497e71722a97b79aeef4e",
"shasum": ""
},
"require": {
@@ -1111,9 +1111,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.352.5"
"source": "https://github.com/aws/aws-sdk-php/tree/3.352.7"
},
"time": "2025-08-08T18:09:38+00:00"
"time": "2025-08-12T18:29:26+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
@@ -1758,16 +1758,16 @@
},
{
"name": "dedoc/scramble",
"version": "v0.12.28",
"version": "v0.12.29",
"source": {
"type": "git",
"url": "https://github.com/dedoc/scramble.git",
"reference": "f06a98d1fd6678544428df7077d73194e2d28de3"
"reference": "b1fecfa9f6dc0094cf716d609975b933c5d522f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/f06a98d1fd6678544428df7077d73194e2d28de3",
"reference": "f06a98d1fd6678544428df7077d73194e2d28de3",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/b1fecfa9f6dc0094cf716d609975b933c5d522f0",
"reference": "b1fecfa9f6dc0094cf716d609975b933c5d522f0",
"shasum": ""
},
"require": {
@@ -1826,7 +1826,7 @@
],
"support": {
"issues": "https://github.com/dedoc/scramble/issues",
"source": "https://github.com/dedoc/scramble/tree/v0.12.28"
"source": "https://github.com/dedoc/scramble/tree/v0.12.29"
},
"funding": [
{
@@ -1834,7 +1834,7 @@
"type": "github"
}
],
"time": "2025-08-04T12:20:10+00:00"
"time": "2025-08-12T12:17:56+00:00"
},
{
"name": "dflydev/dot-access-data",
@@ -2557,7 +2557,7 @@
},
{
"name": "filament/actions",
"version": "v3.3.35",
"version": "v3.3.36",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/actions.git",
@@ -2610,16 +2610,16 @@
},
{
"name": "filament/filament",
"version": "v3.3.35",
"version": "v3.3.36",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/panels.git",
"reference": "d9d2367c910956e1e7a4c2903600f37bd740dd11"
"reference": "6f460f7f5146217b71fc242b288f908fa58c9131"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/d9d2367c910956e1e7a4c2903600f37bd740dd11",
"reference": "d9d2367c910956e1e7a4c2903600f37bd740dd11",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/6f460f7f5146217b71fc242b288f908fa58c9131",
"reference": "6f460f7f5146217b71fc242b288f908fa58c9131",
"shasum": ""
},
"require": {
@@ -2671,20 +2671,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-08-04T10:34:25+00:00"
"time": "2025-08-12T13:15:51+00:00"
},
{
"name": "filament/forms",
"version": "v3.3.35",
"version": "v3.3.36",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/forms.git",
"reference": "158177c4a551c8aba5be3f45bc423195ab28e5bc"
"reference": "6d2eddf754f30dee8730535dfcbcefb4cd5e1136"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/158177c4a551c8aba5be3f45bc423195ab28e5bc",
"reference": "158177c4a551c8aba5be3f45bc423195ab28e5bc",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/6d2eddf754f30dee8730535dfcbcefb4cd5e1136",
"reference": "6d2eddf754f30dee8730535dfcbcefb4cd5e1136",
"shasum": ""
},
"require": {
@@ -2727,20 +2727,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-08-04T10:34:20+00:00"
"time": "2025-08-12T13:15:47+00:00"
},
{
"name": "filament/infolists",
"version": "v3.3.35",
"version": "v3.3.36",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/infolists.git",
"reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa"
"reference": "4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/89a3f1f236863e2035be3d7b0c68987508dd06fa",
"reference": "89a3f1f236863e2035be3d7b0c68987508dd06fa",
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de",
"reference": "4533c2ccb6ef06ab7f27d81e27be0cdd4f5e72de",
"shasum": ""
},
"require": {
@@ -2778,11 +2778,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-06-23T10:46:53+00:00"
"time": "2025-08-12T13:15:27+00:00"
},
{
"name": "filament/notifications",
"version": "v3.3.35",
"version": "v3.3.36",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/notifications.git",
@@ -2834,16 +2834,16 @@
},
{
"name": "filament/support",
"version": "v3.3.35",
"version": "v3.3.36",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/support.git",
"reference": "89d8e729025c195a06f2e510af4517fc9a8fd07f"
"reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/support/zipball/89d8e729025c195a06f2e510af4517fc9a8fd07f",
"reference": "89d8e729025c195a06f2e510af4517fc9a8fd07f",
"url": "https://api.github.com/repos/filamentphp/support/zipball/afafd5e7a2f8cf052f70f989b52d82d0a1df5c78",
"reference": "afafd5e7a2f8cf052f70f989b52d82d0a1df5c78",
"shasum": ""
},
"require": {
@@ -2889,20 +2889,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-07-28T09:02:43+00:00"
"time": "2025-08-12T13:15:44+00:00"
},
{
"name": "filament/tables",
"version": "v3.3.35",
"version": "v3.3.36",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/tables.git",
"reference": "22bc439ec6f2b5fd5703ef499381d7beb0a6b369"
"reference": "20ce6217382785df7b39b8473644c1bfe967963c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/22bc439ec6f2b5fd5703ef499381d7beb0a6b369",
"reference": "22bc439ec6f2b5fd5703ef499381d7beb0a6b369",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/20ce6217382785df7b39b8473644c1bfe967963c",
"reference": "20ce6217382785df7b39b8473644c1bfe967963c",
"shasum": ""
},
"require": {
@@ -2941,11 +2941,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-07-28T09:02:34+00:00"
"time": "2025-08-12T13:15:31+00:00"
},
{
"name": "filament/widgets",
"version": "v3.3.35",
"version": "v3.3.36",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/widgets.git",
@@ -3717,16 +3717,16 @@
},
{
"name": "laravel/framework",
"version": "v12.22.1",
"version": "v12.23.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "d33ee45184126f32f593d4b809a846ed88a1dc43"
"reference": "2a0e9331a0db904236143fe915c281ff4be274a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/d33ee45184126f32f593d4b809a846ed88a1dc43",
"reference": "d33ee45184126f32f593d4b809a846ed88a1dc43",
"url": "https://api.github.com/repos/laravel/framework/zipball/2a0e9331a0db904236143fe915c281ff4be274a3",
"reference": "2a0e9331a0db904236143fe915c281ff4be274a3",
"shasum": ""
},
"require": {
@@ -3767,6 +3767,8 @@
"symfony/mailer": "^7.2.0",
"symfony/mime": "^7.2.0",
"symfony/polyfill-php83": "^1.31",
"symfony/polyfill-php84": "^1.31",
"symfony/polyfill-php85": "^1.31",
"symfony/process": "^7.2.0",
"symfony/routing": "^7.2.0",
"symfony/uid": "^7.2.0",
@@ -3928,7 +3930,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-08-08T13:58:03+00:00"
"time": "2025-08-12T17:35:05+00:00"
},
{
"name": "laravel/helpers",
@@ -10930,6 +10932,158 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php84",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php84.git",
"reference": "000df7860439609837bbe28670b0be15783b7fbf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf",
"reference": "000df7860439609837bbe28670b0be15783b7fbf",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php84\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-02-20T12:04:08+00:00"
},
{
"name": "symfony/polyfill-php85",
"version": "v1.32.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
"reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd",
"reference": "6fedf31ce4e3648f4ff5ca58bfd53127d38f05fd",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php85\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.32.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-05-02T08:40:52+00:00"
},
{
"name": "symfony/polyfill-uuid",
"version": "v1.32.0",

View File

@@ -6,7 +6,7 @@ return [
'logo' => env('APP_LOGO'),
'favicon' => env('APP_FAVICON', '/pelican.ico'),
'version' => 'canary',
'version' => '1.0.0-beta25',
'timezone' => 'UTC',

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('permissions')
->where('name', 'seeIps activity')
->update(['name' => 'seeIps activityLog']);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('permissions')
->where('name', 'seeIps activityLog')
->update(['name' => 'seeIps activity']);
}
};

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('activity_logs', function (Blueprint $table) {
$table->dropColumn('batch');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('activity_logs', function (Blueprint $table) {
$table->string('batch', 36)->nullable();
});
}
};

View File

@@ -13,5 +13,6 @@
root * /var/www/html/public
encode gzip
file_server
php_fastcgi 127.0.0.1:9000
}

View File

@@ -2,7 +2,7 @@
return [
'title' => 'Application API Keys',
'empty_table' => 'No API keys',
'empty' => 'No API keys',
'whitelist' => 'Whitelisted IPv4 Addresses',
'whitelist_help' => 'API keys can be restricted to only work from specific IPv4 addresses. Enter each address on a new line.',
'whitelist_placeholder' => 'Example: 127.0.0.1 or 192.168.1.1',

View File

@@ -23,7 +23,7 @@ return [
],
'export' => [
'modal' => 'How would you like to export :egg ?',
'as' => 'As',
'as' => 'As .:format',
],
'in_use' => 'In Use',
'servers' => 'Servers',

View File

@@ -55,6 +55,6 @@ return [
],
'checks' => [
'successful' => 'Successful',
'failed' => 'Failed',
'failed' => 'Failed :checks',
],
];

View File

@@ -4,6 +4,7 @@ return [
'nav_title' => 'Nodes',
'model_label' => 'Node',
'model_label_plural' => 'Nodes',
'create' => 'Create Node',
'tabs' => [
'overview' => 'Overview',
'basic_settings' => 'Basic Settings',
@@ -29,7 +30,7 @@ return [
'architecture' => 'Architecture',
'kernel' => 'Kernel',
'unknown' => 'Unknown',
'latest' => 'Latest',
'latest' => '(Latest: :version)',
'node_uuid' => 'Node UUID',
'node_id' => 'Node ID',
@@ -88,6 +89,7 @@ return [
'auto_deploy' => 'Auto Deploy Command',
'auto_question' => 'Choose between Standalone and Docker install.',
'auto_label' => 'Type',
'standalone' => 'Standalone',
'docker' => 'Docker',
'auto_command' => 'To auto-configure your node run the following command:',
@@ -112,4 +114,9 @@ return [
'error_connecting' => 'Error connecting to :node',
'error_connecting_description' => 'The configuration could not be automatically updated on Wings, you will need to manually update the configuration file.',
'allocation' => 'Allocation',
'cloudflare_issue' => [
'title' => 'Cloudflare Issue',
'body' => 'Your Node is not accessible by Cloudflare',
],
];

View File

@@ -1,15 +0,0 @@
<?php
return [
'model_label' => 'Schedule',
'model_label_plural' => 'Schedule',
'import' => [
'file' => 'File',
'url' => 'URL',
'schedule_help' => 'This should be the raw .json file ( schedule-daily-restart.json )',
'url_help' => 'URLs must point directly to the raw .json file',
'add_url' => 'New URL',
'import_failed' => 'Import Failed',
'import_success' => 'Import Success',
],
];

View File

@@ -5,6 +5,7 @@ return [
'model_label' => 'Server',
'model_label_plural' => 'Servers',
'no_servers' => 'No Servers',
'create' => 'Create Server',
'next_step' => 'Next Step',
'ip_address' => 'IP Address',
'ip_address_helper' => 'Usually your machine\'s public IP unless you are port forwarding.',
@@ -111,6 +112,7 @@ return [
'notifications' => [
'server_suspension' => 'Server Suspension',
'server_suspended' => 'Server has been suspended',
'server_already_suspended' => 'Server is already suspended!',
'server_suspend_help' => 'This will suspend the Server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the Server through the panel or API.',
'server_unsuspend_help' => 'This will unsuspend the Server and restore normal user access.',
'server_unsuspended' => 'Server has been unsuspended',
@@ -132,6 +134,9 @@ return [
'reinstall_started' => 'Reinstall started',
'reinstall_failed' => 'Could not start reinstall',
'log_failed' => 'Could not connect to Wings to retrieve server install log.',
'transfer_started' => 'Transfer started',
'transfer_failed' => 'Transfer failed',
'already_transfering' => 'Server is currently being transferred.',
],
'notes' => 'Notes',
'no_notes' => 'No Notes',

View File

@@ -89,6 +89,7 @@ return [
],
'oauth' => [
'enable' => 'Enable',
'enable_schema' => 'Enable :schema',
'disable' => 'Disable',
'client_id' => 'Client ID',
'client_secret' => 'Client Secret',
@@ -98,6 +99,7 @@ return [
'display_name' => 'Display Name',
'auth_url' => 'Authorization callback URL',
'create_missing_users' => 'Auto Create Missing Users?',
'link_missing_users' => 'Auto Link Missing Users?',
],
'misc' => [
'auto_allocation' => [

View File

@@ -14,10 +14,7 @@ return [
'ask_password' => 'Password',
'ask_password_tip' => 'If you would like to create an account with a random password emailed to the user, re-run this command (CTRL+C) and pass the `--no-password` flag.',
'ask_password_help' => 'Passwords must be at least 8 characters in length and contain at least one capital letter and number.',
'2fa_help_text' => [
'This command will disable 2-factor authentication for a user\'s account if it is enabled. This should only be used as an account recovery command if the user is locked out of their account.',
'If this is not what you wanted to do, press CTRL+C to exit this process.',
],
'2fa_help_text' => 'This command will disable 2-factor authentication for a user\'s account if it is enabled. This should only be used as an account recovery command if the user is locked out of their account. If this is not what you wanted to do, press CTRL+C to exit this process.',
'2fa_disabled' => '2-Factor authentication has been disabled for :email.',
],
'schedule' => [

View File

@@ -54,13 +54,13 @@ return [
'schedule' => [
'process' => [
'no_tasks' => 'There are no scheduled tasks for servers that need to be run.',
'error_message' => 'An error was encountered while processing Schedule: ',
'error_message' => 'An error was encountered while processing Schedule: :schedules',
],
],
'upgrade' => [
'integrity' => 'This command does not verify the integrity of downloaded assets. Please ensure that you trust the download source before continuing. If you do not wish to download an archive, please indicate that using the --skip-download flag, or answering "no" to the question below.',
'source_url' => 'Download Source (set with --url=):',
'php_version' => 'Cannot execute self-upgrade process. The minimum required PHP version required is 7.4.0, you have',
'php_version' => 'Cannot execute self-upgrade process. The minimum required PHP version required is 7.4.0, you have :current',
'skipDownload' => 'Would you like to download and unpack the archive files for the latest version?',
'webserver_user' => 'Your webserver user has been detected as <fg=blue>[{:user}]:</> is this correct?',
'name_webserver' => 'Please enter the name of the user running your webserver process. This varies from system to system, but is generally "www-data", "nginx", or "apache".',

103
lang/en/installer.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
return [
'title' => 'Panel Installer',
'requirements' => [
'title' => 'Server Requirements',
'sections' => [
'version' => [
'title' => 'PHP Version',
'or_newer' => ':version or newer',
'content' => 'Your PHP Version is :version.',
],
'extensions' => [
'title' => 'PHP Extensions',
'good' => 'All needed PHP Extensions are installed.',
'bad' => 'The following PHP Extensions are missing: :extensions',
],
'permissions' => [
'title' => 'Folder Permissions',
'good' => 'All Folders have the correct permissions.',
'bad' => 'The following Folders have wrong permissions: :folders',
],
],
'exception' => 'Some requirements are missing',
],
'environment' => [
'title' => 'Environment',
'fields' => [
'app_name' => 'App Name',
'app_name_help' => 'This will be the Name of your Panel.',
'app_url' => 'App URL',
'app_url_help' => 'This will be the URL you access your Panel from.',
'account' => [
'section' => 'Admin User',
'email' => 'E-Mail',
'username' => 'Username',
'password' => 'Password',
],
],
],
'database' => [
'title' => 'Database',
'driver' => 'Database Driver',
'driver_help' => 'The driver used for the panel database. We recommend "SQLite".',
'fields' => [
'host' => 'Database Host',
'host_help' => 'The host of your database. Make sure it is reachable.',
'port' => 'Database Port',
'port_help' => 'The port of your database.',
'path' => 'Database Path',
'path_help' => 'The path of your .sqlite file relative to the database folder.',
'name' => 'Database Name',
'name_help' => 'The name of the panel database.',
'username' => 'Database Username',
'username_help' => 'The name of your database user.',
'password' => 'Database Password',
'password_help' => 'The password of your database user. Can be empty.',
],
'exceptions' => [
'connection' => 'Database connection failed',
'migration' => 'Migrations failed',
],
],
'session' => [
'title' => 'Session',
'driver' => 'Session Driver',
'driver_help' => 'The driver used for storing sessions. We recommend "Filesystem" or "Database".',
],
'cache' => [
'title' => 'Cache',
'driver' => 'Cache Driver',
'driver_help' => 'The driver used for caching. We recommend "Filesystem".',
'fields' => [
'host' => 'Redis Host',
'host_help' => 'The host of your redis server. Make sure it is reachable.',
'port' => 'Redis Port',
'port_help' => 'The port of your redis server.',
'username' => 'Redis Username',
'username_help' => 'The name of your redis user. Can be empty',
'password' => 'Redis Password',
'password_help' => 'The password for your redis user. Can be empty.',
],
'exception' => 'Redis connection failed',
],
'queue' => [
'title' => 'Queue',
'driver' => 'Queue Driver',
'driver_help' => 'The driver used for handling queues. We recommend "Database".',
'fields' => [
'done' => 'I have done both steps below.',
'done_validation' => 'You need to do both steps before continuing!',
'crontab' => 'Run the following command to set up your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!',
'service' => 'To setup the queue worker service you simply have to run the following command.',
],
],
'exceptions' => [
'write_env' => 'Could not write to .env file',
'migration' => 'Could not run migrations',
'create_user' => 'Could not create admin user',
],
'next_step' => 'Next Step',
'finish' => 'Finish',
];

18
lang/en/notifications.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
return [
'open_server' => 'Open Server',
'installation_completed' => 'Server Installation Completed',
'installation_failed' => 'Server Installation Failed',
'reinstallation_completed' => 'Server Reinstallation Completed',
'reinstallation_failed' => 'Server Reinstallation Failed',
'failed' => 'Failed',
'user_added' => [
'title' => 'Added to Server',
'body' => 'You have been added as a subuser to :server.',
],
'user_removed' => [
'title' => 'Removed from Server',
'body' => 'You have been removed as a subuser from :server.',
],
];

View File

@@ -21,12 +21,12 @@ return [
'timezone' => 'Timezone',
'language' => 'Language',
'language_help' => 'Your language :state has not been translated yet!',
'link' => 'Link ',
'unlink' => 'Unlink ',
'link' => 'Link :name',
'unlink' => 'Unlink :name',
'unlinked' => ':name unlinked',
'scan_qr' => 'Scan QR Code',
'code' => 'Code',
'setup_key' => 'Setup Key',
'setup_key' => 'Setup Key: :secret',
'invalid_code' => 'Invalid 2FA Code',
'code_help' => 'Scan the QR code above using your two-step authentication app, then enter the code generated.',
'2fa_enabled' => 'Two Factor Authentication is currently enabled!',

View File

@@ -29,6 +29,11 @@ return [
'unlock' => 'Unlock',
],
'download' => 'Download',
'rename' => [
'title' => 'Rename',
'new_name' => 'Backup Name',
'notification_success' => 'Backup Renamed Successfully',
],
'restore' => [
'title' => 'Restore',
'helper' => 'Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.',

View File

@@ -36,4 +36,8 @@ return [
'offline' => 'Offline',
'missing' => 'Missing',
],
'websocket_error' => [
'title' => 'Could not connect to websocket!',
'body' => 'Check your browser console for more details.',
],
];

View File

@@ -3,15 +3,17 @@
return [
'title' => 'Servers',
'list' => 'Server List',
'my_servers' => 'My Servers',
'other_servers' => 'Others\' Servers',
'all_servers' => 'All Servers',
'tabs' => [
'my' => 'My Servers',
'other' => 'Others\' Servers',
'all' => 'All Servers',
],
'empty_own' => 'You don\'t own any servers!',
'empty_other' => 'You don\'t have access to any servers!',
'status' => 'Status',
'server' => 'Server',
'resource' => 'Resource',
'resources' => 'Resources',
'usage_limit' => 'Usage Limit: :resource',
'cpu' => 'CPU',
@@ -22,4 +24,7 @@ return [
'loading' => 'Loading...',
'power_actions' => 'Power Actions',
'power_action_sent' => ':action sent to :name',
'copied' => 'Copied to clipboard',
];

View File

@@ -2,6 +2,7 @@
return [
'title' => 'Databases',
'empty' => 'No Databases',
'create_database' => 'Create Database',
'limit' => 'Database limit reached',
'viewing' => 'Viewing: :database',

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