Compare commits

..

1 Commits

Author SHA1 Message Date
Charles
c1fa74ebfd WIP 2026-01-14 13:21:01 -05:00
766 changed files with 9898 additions and 18796 deletions

View File

@@ -61,17 +61,14 @@ jobs:
- name: Create SQLite file
run: touch database/testing.sqlite
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration
mysql:
name: MySQL
@@ -123,20 +120,14 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@@ -191,20 +182,14 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@@ -253,7 +238,6 @@ jobs:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -266,14 +250,11 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests
run: vendor/bin/pest tests/Unit --parallel
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration --parallel
run: vendor/bin/pest tests/Integration

View File

@@ -38,7 +38,7 @@ RUN yarn config set network-timeout 300000 \
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=docker/ . ./
COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize
@@ -50,7 +50,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=docker/ . ./
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
@@ -62,34 +62,38 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
# packages for running the panel
caddy ca-certificates supervisor supercronic fcgi \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
caddy ca-certificates supervisor supercronic fcgi
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Create and remove directories
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
&& rm -rf /var/www/html/plugins \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env /var/www/html/.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -s /pelican-data/storage /var/www/html/public/storage \
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
&& ln -s /pelican-data/plugins /var/www/html \
# Allow www-data write permissions where necessary
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
# Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /pelican-data/plugins /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
&& ln -s /pelican-data/plugins /var/www/html/plugins \
# 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 \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/crontabs/crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh

View File

@@ -5,6 +5,6 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath gd intl zip pcntl pdo_mysql pdo_pgsql bz2
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions

View File

@@ -5,7 +5,7 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath gd intl zip pcntl pdo_mysql pdo_pgsql bz2
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions
@@ -42,7 +42,7 @@ RUN yarn config set network-timeout 300000 \
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=docker/ . ./
COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize
@@ -54,7 +54,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=docker/ . ./
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
@@ -68,33 +68,36 @@ WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
# packages for running the panel
caddy ca-certificates supervisor supercronic fcgi coreutils \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
caddy ca-certificates supervisor supercronic fcgi coreutils
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Create and remove directories
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
&& rm -rf /var/www/html/plugins \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env /var/www/html/.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -s /pelican-data/storage /var/www/html/public/storage \
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
&& ln -s /pelican-data/plugins /var/www/html \
# Allow www-data write permissions where necessary
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/
# Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /pelican-data/plugins /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, storage, and plugins
&& ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
&& ln -s /pelican-data/plugins /var/www/html/plugins \
# 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 \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/crontabs/crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh

View File

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

View File

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

View File

@@ -10,7 +10,6 @@ use App\Notifications\MailTested;
use App\Traits\EnvironmentWriterTrait;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use BackedEnum;
use Exception;
use Filament\Actions\Action;
@@ -54,7 +53,6 @@ class Settings extends Page implements HasSchemas
CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions;
}
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
use EnvironmentWriterTrait;
use InteractsWithForms;
@@ -98,7 +96,11 @@ class Settings extends Page implements HasSchemas
return trans('admin/setting.title');
}
/** @return array<Component> */
/**
* @return array<Component>
*
* @throws Exception
*/
protected function getFormSchema(): array
{
return [
@@ -106,44 +108,34 @@ class Settings extends Page implements HasSchemas
->columns()
->persistTabInQueryString()
->disabled(fn () => !user()?->can('update settings'))
->tabs($this->getTabs()),
];
}
/**
* @return Tab[]
*
* @throws Exception
*/
protected function getDefaultTabs(): array
{
return [
Tab::make('general')
->label(trans('admin/setting.navigation.general'))
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('captcha')
->label(trans('admin/setting.navigation.captcha'))
->icon('tabler-shield')
->schema($this->captchaSettings())
->columns(1),
Tab::make('mail')
->label(trans('admin/setting.navigation.mail'))
->icon('tabler-mail')
->schema($this->mailSettings()),
Tab::make('backup')
->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth')
->schema($this->oauthSettings())
->columns(1),
Tab::make('misc')
->label(trans('admin/setting.navigation.misc'))
->icon('tabler-tool')
->schema($this->miscSettings()),
->tabs([
Tab::make('general')
->label(trans('admin/setting.navigation.general'))
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('captcha')
->label(trans('admin/setting.navigation.captcha'))
->icon('tabler-shield')
->schema($this->captchaSettings())
->columns(1),
Tab::make('mail')
->label(trans('admin/setting.navigation.mail'))
->icon('tabler-mail')
->schema($this->mailSettings()),
Tab::make('backup')
->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth')
->schema($this->oauthSettings())
->columns(1),
Tab::make('misc')
->label(trans('admin/setting.navigation.misc'))
->icon('tabler-tool')
->schema($this->miscSettings()),
]),
];
}

View File

@@ -9,7 +9,7 @@ use App\Filament\Components\Forms\Fields\MonacoEditor;
use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Checkbox;
@@ -37,7 +37,6 @@ class CreateEgg extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
protected static string $resource = EggResource::class;
@@ -58,231 +57,227 @@ class CreateEgg extends CreateRecord
return [];
}
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make()
->tabs($this->getTabs())
->columnSpanFull()
->persistTabInQueryString(),
]);
}
/** @return Tab[] */
protected function getDefaultTabs(): array
{
return [
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->maxLength(255)
->required()
->email()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.author_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(2)
->columnSpanFull()
->helperText(trans('admin/egg.description_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->keyPlaceholder('Default')
->valueLabel(trans('admin/egg.startup_command'))
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->default(1),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->keyPlaceholder('Java 21')
->valueLabel(trans('admin/egg.docker_uri'))
->valuePlaceholder('ghcr.io/pelican-eggs/yolks:java_21')
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->required()
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->default('{}')
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->default('{}')
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
->hiddenLabel()
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->reorderable()->orderColumn()
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->maxLength(255)
->required()
->email()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->helperText(trans('admin/egg.author_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(2)
->columnSpanFull()
->helperText(trans('admin/egg.description_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->keyPlaceholder('Default')
->valueLabel(trans('admin/egg.startup_command'))
->valuePlaceholder('java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}')
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip')
->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged')
->default(1),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->keyPlaceholder('Java 21')
->valueLabel(trans('admin/egg.docker_uri'))
->valuePlaceholder('ghcr.io/pelican-eggs/yolks:java_21')
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->required()
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->default('{}')
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->default('{}')
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
->hiddenLabel()
->addActionLabel(trans('admin/egg.add_new_variable'))
->grid()
->relationship('variables')
->reorderable()->orderColumn()
->collapsible()->collapsed()
->columnSpan(2)
->defaultItems(0)
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
TextInput::make('name')
->label(trans('admin/egg.name'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
]),
]),
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->default('bash')
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))
->language(EditorLanguages::shell)
->columnSpanFull()
->lazy(),
]),
];
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->default('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->default('bash')
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))
->language(EditorLanguages::shell)
->columnSpanFull()
->lazy(),
]),
])->columnSpanFull()->persistTabInQueryString(),
]);
}
protected function handleRecordCreation(array $data): Model

View File

@@ -12,7 +12,6 @@ use App\Models\Egg;
use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeTabs;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@@ -48,398 +47,391 @@ class EditEgg extends EditRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeTabs;
protected static string $resource = EggResource::class;
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Tabs::make()
->tabs($this->getTabs())
->columnSpanFull()
->persistTabInQueryString(),
]);
}
/** @return Tab[] */
protected function getDefaultTabs(): array
{
return [
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon('tabler-egg')
->schema([
Grid::make(2)
->columnSpan(1)
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 6])
->icon('tabler-egg')
->schema([
Image::make('', '')
->hidden(fn ($record) => !$record->image)
->url(fn ($record) => $record->image)
->alt('')
->alignJustify()
->imageSize(150)
->columnSpanFull(),
Flex::make([
Action::make('uploadImage')
->iconButton()
->iconSize(IconSize::Large)
->icon('tabler-photo-up')
->modal()
->modalHeading('')
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('imageUrl'),
Hidden::make('imageExtension'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
$set('imageUrl', null);
$set('imageExtension', null);
Grid::make(2)
->columnSpan(1)
->schema([
Image::make('', '')
->hidden(fn ($record) => !$record->image)
->url(fn ($record) => $record->image)
->alt('')
->alignJustify()
->imageSize(150)
->columnSpanFull(),
Flex::make([
Action::make('uploadImage')
->iconButton()
->iconSize(IconSize::Large)
->icon('tabler-photo-up')
->modal()
->modalHeading('')
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
->schema([
Tabs::make()
->contained(false)
->tabs([
Tab::make(trans('admin/egg.import.url'))
->schema([
Hidden::make('imageUrl'),
Hidden::make('imageExtension'),
TextInput::make('image_url')
->label(trans('admin/egg.import.image_url'))
->reactive()
->autocomplete(false)
->debounce(500)
->afterStateUpdated(function ($state, Set $set) {
if (!$state) {
$set('image_url_error', null);
$set('imageUrl', null);
$set('imageExtension', null);
return;
}
return;
}
try {
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
try {
if (!filter_var($state, FILTER_VALIDATE_URL)) {
throw new Exception(trans('admin/egg.import.invalid_url'));
}
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
}
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
}
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
$host = parse_url($state, PHP_URL_HOST);
$ip = gethostbyname($host);
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new Exception(trans('admin/egg.import.no_local_ip'));
}
if (
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
) {
throw new Exception(trans('admin/egg.import.no_local_ip'));
}
$set('imageUrl', $state);
$set('imageExtension', $extension);
$set('image_url_error', null);
$set('imageUrl', $state);
$set('imageExtension', $extension);
$set('image_url_error', null);
} catch (Exception $e) {
$set('image_url_error', $e->getMessage());
$set('imageUrl', null);
$set('imageExtension', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn ($get) => $get('image_url_error') !== null)
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
} catch (Exception $e) {
$set('image_url_error', $e->getMessage());
$set('imageUrl', null);
$set('imageExtension', null);
}
}),
TextEntry::make('image_url_error')
->hiddenLabel()
->visible(fn ($get) => $get('image_url_error') !== null)
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
Image::make(fn (Get $get) => $get('image_url'), '')
->imageSize(150)
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
->alignCenter(),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->disk('public')
->directory(Egg::ICON_STORAGE_PATH)
->acceptedFileTypes([
'image/png',
'image/jpeg',
'image/webp',
'image/svg+xml',
])
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
return $record->uuid . '.' . $file->getClientOriginalExtension();
}),
]),
]),
Tab::make(trans('admin/egg.import.file'))
->schema([
FileUpload::make('image')
->hiddenLabel()
->previewable()
->openable(false)
->downloadable(false)
->maxSize(256)
->maxFiles(1)
->columnSpanFull()
->alignCenter()
->imageEditor()
->image()
->disk('public')
->directory(Egg::ICON_STORAGE_PATH)
->acceptedFileTypes([
'image/png',
'image/jpeg',
'image/webp',
'image/svg+xml',
])
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
return $record->uuid . '.' . $file->getClientOriginalExtension();
}),
]),
]),
])
->action(function (array $data, $record): void {
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
])
->action(function (array $data, $record): void {
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
return;
}
if (!empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
if (!empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.image_updated'))
->success()
->send();
return;
}
return;
}
if (empty($data['imageUrl']) && empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('delete_image')
->visible(fn ($record) => $record->image)
->hiddenLabel()
->icon('tabler-trash')
->iconButton()
->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
if (empty($data['imageUrl']) && empty($data['image'])) {
Notification::make()
->title(trans('admin/egg.import.no_image'))
->warning()
->send();
}
}),
Action::make('delete_image')
->visible(fn ($record) => $record->image)
->hiddenLabel()
->icon('tabler-trash')
->iconButton()
->iconSize(IconSize::Large)
->color('danger')
->action(function ($record) {
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
if (Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
Notification::make()
->title(trans('admin/egg.import.image_deleted'))
->success()
->send();
Notification::make()
->title(trans('admin/egg.import.image_deleted'))
->success()
->send();
$record->refresh();
}),
]),
]),
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
->helperText(trans('admin/egg.description_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->columnSpan(1)
->disabled(),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->valueLabel(trans('admin/egg.startup_command'))
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon('tabler-server-cog')
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon('tabler-variable')
->schema([
Repeater::make('variables')
->hiddenLabel()
->grid()
->relationship('variables')
->reorderable()
->collapsible()->collapsed()
->orderColumn()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
$record->refresh();
}),
]),
]),
TextInput::make('name')
->label(trans('admin/egg.name'))
->required()
->maxLength(255)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 3, 'lg' => 2])
->helperText(trans('admin/egg.name_help')),
Textarea::make('description')
->label(trans('admin/egg.description'))
->rows(3)
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 4, 'lg' => 3])
->helperText(trans('admin/egg.description_help')),
TextInput::make('id')
->label(trans('admin/egg.egg_id'))
->columnSpan(1)
->disabled(),
TextInput::make('uuid')
->label(trans('admin/egg.egg_uuid'))
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.uuid_help')),
TextInput::make('author')
->label(trans('admin/egg.author'))
->required()
->maxLength(255)
->email()
->disabled()
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 2])
->helperText(trans('admin/egg.author_help_edit')),
Toggle::make('force_outgoing_ip')
->inline(false)
->label(trans('admin/egg.force_ip'))
->columnSpan(1)
->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
KeyValue::make('startup_commands')
->label(trans('admin/egg.startup_commands'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->required()
->addActionLabel(trans('admin/egg.add_startup'))
->keyLabel(trans('admin/egg.startup_name'))
->valueLabel(trans('admin/egg.startup_command'))
->helperText(trans('admin/egg.startup_help')),
TagsInput::make('file_denylist')
->label(trans('admin/egg.file_denylist'))
->placeholder('denied-file.txt')
->helperText(trans('admin/egg.file_denylist_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TextInput::make('update_url')
->label(trans('admin/egg.update_url'))
->url()
->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
TagsInput::make('features')
->label(trans('admin/egg.features'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags')
->label(trans('admin/egg.tags'))
->columnSpan(['default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 3]),
KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images'))
->live()
->columnSpanFull()
->required()
->addActionLabel(trans('admin/egg.add_image'))
->keyLabel(trans('admin/egg.docker_name'))
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns()
->icon('tabler-server-cog')
->schema([
CopyFrom::make('copy_process_from')
->process(),
TextInput::make('config_stop')
->label(trans('admin/egg.stop_command'))
->maxLength(255)
->helperText(trans('admin/egg.stop_command_help')),
Textarea::make('config_startup')->rows(10)->json()
->label(trans('admin/egg.start_config'))
->helperText(trans('admin/egg.start_config_help')),
Textarea::make('config_files')->rows(10)->json()
->label(trans('admin/egg.config_files'))
->helperText(trans('admin/egg.config_files_help')),
Textarea::make('config_logs')->rows(10)->json()
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon('tabler-variable')
->schema([
Repeater::make('variables')
->hiddenLabel()
->grid()
->relationship('variables')
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
->collapsible()->collapsed()
->orderColumn()
->addActionLabel(trans('admin/egg.add_new_variable'))
->itemLabel(fn (array $state) => $state['name'])
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
return $data;
})
->schema([
TextInput::make('name')
->label(trans('admin/egg.name'))
->live()
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
])
->required(),
Textarea::make('description')->label(trans('admin/egg.description'))->columnSpanFull(),
TextInput::make('env_variable')
->label(trans('admin/egg.environment_variable'))
->maxLength(255)
->prefix('{{')
->suffix('}}')
->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([
'unique' => trans('admin/egg.error_unique'),
'required' => trans('admin/egg.error_required'),
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
Checkbox::make('user_editable')->label(trans('admin/egg.editable')),
]),
TagsInput::make('rules')
->label(trans('admin/egg.rules'))
->columnSpanFull()
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
]),
]),
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon('tabler-file-download')
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->hiddenLabel()
->language(EditorLanguages::shell)
->columnSpanFull(),
]),
];
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon('tabler-file-download')
->schema([
CopyFrom::make('copy_script_from')
->script(),
TextInput::make('script_container')
->label(trans('admin/egg.script_container'))
->required()
->maxLength(255)
->placeholder('ghcr.io/pelican-eggs/installers:debian'),
Select::make('script_entry')
->label(trans('admin/egg.script_entry'))
->selectablePlaceholder(false)
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(),
MonacoEditor::make('script_install')
->hiddenLabel()
->language(EditorLanguages::shell)
->columnSpanFull(),
]),
])->columnSpanFull()->persistTabInQueryString(),
]);
}
/** @return array<Action|ActionGroup> */
@@ -449,16 +441,6 @@ class EditEgg extends EditRecord
DeleteAction::make()
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? trans('filament-actions::delete.single.label') : trans('admin/egg.in_use'))
->successNotification(fn (Egg $egg) => Notification::make()
->success()
->title(trans('admin/egg.delete_success'))
->body(trans('admin/egg.deleted', ['egg' => $egg->name]))
)
->failureNotification(fn (Egg $egg) => Notification::make()
->danger()
->title(trans('admin/egg.delete_failed'))
->body(trans('admin/egg.could_not_delete', ['egg' => $egg->name]))
)
->iconButton()->iconSize(IconSize::ExtraLarge),
ExportEggAction::make(),
ImportEggAction::make()

View File

@@ -18,13 +18,11 @@ use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ReplicateAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class ListEggs extends ListRecords
@@ -90,45 +88,15 @@ class ListEggs extends ListRecords
])
->groupedBulkActions([
DeleteBulkAction::make()
->before(function (Collection &$records) {
$eggsWithServers = $records->filter(fn (Egg $egg) => $egg->servers_count > 0);
if ($eggsWithServers->isNotEmpty()) {
$eggNames = $eggsWithServers->map(fn (Egg $egg) => sprintf('%s (%d server%s)', $egg->name, $egg->servers_count, $egg->servers_count > 1 ? 's' : ''))
->join(', ');
Notification::make()
->danger()
->title(trans('admin/egg.cannot_delete', ['count' => $eggsWithServers->count()]))
->body(trans('admin/egg.eggs_have_servers', ['eggs' => $eggNames]))
->send();
}
$records = $records->filter(fn (Egg $egg) => $egg->servers_count <= 0);
if ($records->isEmpty()) {
$this->halt();
}
}),
->before(fn (&$records) => $records = $records->filter(function ($egg) {
/** @var Egg $egg */
return $egg->servers_count <= 0;
})),
UpdateEggBulkAction::make()
->before(function (Collection &$records) {
$eggsWithoutUpdateUrl = $records->filter(fn (Egg $egg) => $egg->update_url === null);
if ($eggsWithoutUpdateUrl->isNotEmpty()) {
$eggNames = $eggsWithoutUpdateUrl->pluck('name')->join(', ');
Notification::make()
->warning()
->title(trans('admin/egg.cannot_update', ['count' => $eggsWithoutUpdateUrl->count()]))
->body(trans('admin/egg.no_update_url', ['eggs' => $eggNames]))
->send();
}
$records = $records->filter(fn (Egg $egg) => $egg->update_url !== null);
if ($records->isEmpty()) {
$this->halt();
}
}),
->before(fn (&$records) => $records = $records->filter(function ($egg) {
/** @var Egg $egg */
return cache()->get("eggs.$egg->uuid.update", false);
})),
])
->emptyStateIcon('tabler-eggs')
->emptyStateDescription('')

View File

@@ -21,6 +21,7 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Schema;
@@ -150,32 +151,30 @@ class MountResource extends Resource
->label(trans('admin/mount.description'))
->helperText(trans('admin/mount.description_help'))
->columnSpanFull(),
])
->columnSpan([
'default' => 1,
'lg' => 2,
])
->columns([
'default' => 1,
'xl' => 2,
])->columnSpan(1)->columns([
'default' => 1,
'lg' => 2,
]),
Group::make()->schema([
Section::make()->schema([
Select::make('eggs')->multiple()
->label(trans('admin/mount.eggs'))
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
->preload(),
Select::make('nodes')->multiple()
->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
->searchable(['name', 'fqdn'])
->preload(),
]),
Section::make()->schema([
Select::make('eggs')
->multiple()
->label(trans('admin/mount.eggs'))
// Selecting only non-json fields to prevent Postgres from choking on DISTINCT JSON columns
->relationship('eggs', 'name', fn (Builder $query) => $query->select(['eggs.id', 'eggs.name']))
->preload(),
Select::make('nodes')
->multiple()
->label(trans('admin/mount.nodes'))
->relationship('nodes', 'name', fn (Builder $query) => $query->whereIn('nodes.id', user()?->accessibleNodes()->pluck('id')))
->searchable(['name', 'fqdn'])
->preload(),
])->columns([
'default' => 1,
'lg' => 2,
]),
])->columns([
'default' => 1,
'lg' => 3,
'lg' => 2,
]);
}

View File

@@ -6,7 +6,7 @@ use App\Filament\Admin\Resources\Nodes\NodeResource;
use App\Models\Node;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use App\Traits\Filament\CanCustomizeSteps;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\TagsInput;
@@ -27,397 +27,392 @@ class CreateNode extends CreateRecord
{
use CanCustomizeHeaderActions;
use CanCustomizeHeaderWidgets;
use CanCustomizeSteps;
protected static string $resource = NodeResource::class;
protected static bool $canCreateAnother = false;
/**
* @throws Exception
*/
public function form(Schema $schema): Schema
{
return $schema
->components([
Wizard::make($this->getSteps())
->columnSpanFull()
->nextAction(fn (Action $action) => $action->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-right'))
Wizard::make([
Step::make('basic')
->label(trans('admin/node.tabs.basic_settings'))
->icon('tabler-server')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) {
if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.fqdn_help');
}
return '';
}
return trans('admin/node.error');
})
->hintColor('danger')
->hint(function ($state) {
if (is_ip($state) && request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
return '';
})
->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
}
})
->maxLength(255),
TextInput::make('ip')
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => trans('admin/node.valid'),
false => trans('admin/node.invalid'),
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
TextInput::make('daemon_connect')
->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer(),
TextInput::make('name')
->label(trans('admin/node.display_name'))
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection')
->label(trans('admin/node.ssl'))
->columnSpan(1)
->inline()
->helperText(function (Get $get) {
if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl'));
}
if (is_ip($get('fqdn'))) {
return trans('admin/node.ssl_help');
}
return '';
})
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([
'http' => 'HTTP',
'https' => 'HTTPS (SSL)',
'https_proxy' => 'HTTPS with (reverse) proxy',
])
->colors([
'http' => 'warning',
'https' => 'success',
'https_proxy' => 'success',
])
->icons([
'http' => 'tabler-lock-open-off',
'https' => 'tabler-lock',
'https_proxy' => 'tabler-shield-lock',
])
->default(fn () => request()->isSecure() ? 'https' : 'http')
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings'))
->icon('tabler-server-cog')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
ToggleButtons::make('maintenance_mode')
->label(trans('admin/node.maintenance_mode'))->inline()
->columnSpan(1)
->default(false)
->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help'))
->options([
true => trans('admin/node.enabled'),
false => trans('admin/node.disabled'),
])
->colors([
true => 'danger',
false => 'success',
]),
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label(trans('admin/node.use_for_deploy'))->inline()
->options([
true => trans('admin/node.yes'),
false => trans('admin/node.no'),
])
->colors([
true => 'success',
false => 'danger',
]),
TagsInput::make('tags')
->label(trans('admin/node.tags'))
->columnSpan(2),
TextInput::make('upload_size')
->label(trans('admin/node.upload_limit'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help'))
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label(trans('admin/node.sftp_port'))
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(2)
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_mem')
->dehydrated()
->label(trans('admin/node.memory'))->inlineLabel()->inline()
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label(trans('admin/node.memory_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label(trans('admin/node.overallocate'))->inlineLabel()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_disk')
->dehydrated()
->label(trans('admin/node.disk'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.disk_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->dehydrated()
->label(trans('admin/node.cpu'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.cpu_limit'))->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0)
->required(),
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%')
->required(),
]),
]),
])->columnSpanFull()
->nextAction(fn (Action $action) => $action->label(trans('admin/node.next_step'))->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-right'))
->previousAction(fn (Action $action) => $action->iconButton()->iconSize(IconSize::ExtraLarge)->icon('tabler-arrow-left'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::icon-button
type="submit"
iconSize="xl"
icon="tabler-file-plus"
>
{{ trans('admin/node.create') }}
</x-filament::icon-button>
BLADE))),
<x-filament::icon-button
type="submit"
iconSize="xl"
icon="tabler-file-plus"
>
{{ trans('admin/node.create') }}
</x-filament::icon-button>
BLADE))),
]);
}
/** @return Step[] */
protected function getDefaultSteps(): array
{
return [
Step::make('basic')
->label(trans('admin/node.tabs.basic_settings'))
->icon('tabler-server')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
TextInput::make('fqdn')
->columnSpan(2)
->required()
->autofocus()
->live(debounce: 1500)
->rules(Node::getRulesForField('fqdn'))
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
->helperText(function ($state) {
if (is_ip($state)) {
if (request()->isSecure()) {
return trans('admin/node.fqdn_help');
}
return '';
}
return trans('admin/node.error');
})
->hintColor('danger')
->hint(function ($state) {
if (is_ip($state) && request()->isSecure()) {
return trans('admin/node.ssl_ip');
}
return '';
})
->afterStateUpdated(function (Set $set, ?string $state) {
$set('dns', null);
$set('ip', null);
[$subdomain] = str($state)->explode('.', 2);
if (!is_numeric($subdomain)) {
$set('name', $subdomain);
}
if (!$state || is_ip($state)) {
$set('dns', null);
return;
}
$ip = get_ip_from_hostname($state);
if ($ip) {
$set('dns', true);
$set('ip', $ip);
} else {
$set('dns', false);
}
})
->maxLength(255),
TextInput::make('ip')
->disabled()
->hidden(),
ToggleButtons::make('dns')
->label(trans('admin/node.dns'))
->helperText(trans('admin/node.dns_help'))
->disabled()
->inline()
->default(null)
->hint(fn (Get $get) => $get('ip'))
->hintColor('success')
->options([
true => trans('admin/node.valid'),
false => trans('admin/node.invalid'),
])
->colors([
true => 'success',
false => 'danger',
])
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
TextInput::make('daemon_connect')
->columnSpan(1)
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer(),
TextInput::make('name')
->label(trans('admin/node.display_name'))
->columnSpan([
'default' => 1,
'sm' => 1,
'md' => 1,
'lg' => 2,
])
->required()
->maxLength(100),
Hidden::make('scheme')
->default(fn () => request()->isSecure() ? 'https' : 'http'),
Hidden::make('behind_proxy')
->default(false),
ToggleButtons::make('connection')
->label(trans('admin/node.ssl'))
->columnSpan(1)
->inline()
->helperText(function (Get $get) {
if (request()->isSecure()) {
return new HtmlString(trans('admin/node.panel_on_ssl'));
}
if (is_ip($get('fqdn'))) {
return trans('admin/node.ssl_help');
}
return '';
})
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
->options([
'http' => 'HTTP',
'https' => 'HTTPS (SSL)',
'https_proxy' => 'HTTPS with (reverse) proxy',
])
->colors([
'http' => 'warning',
'https' => 'success',
'https_proxy' => 'success',
])
->icons([
'http' => 'tabler-lock-open-off',
'https' => 'tabler-lock',
'https_proxy' => 'tabler-shield-lock',
])
->default(fn () => request()->isSecure() ? 'https' : 'http')
->live()
->dehydrated(false)
->afterStateUpdated(function ($state, Set $set) {
$set('scheme', $state === 'http' ? 'http' : 'https');
$set('behind_proxy', $state === 'https_proxy');
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
$set('daemon_listen', 8080);
}),
TextInput::make('daemon_listen')
->columnSpan(1)
->label(trans('admin/node.listen_port'))
->helperText(trans('admin/node.listen_port_help'))
->minValue(1)
->maxValue(65535)
->default(8080)
->required()
->integer()
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
]),
Step::make('advanced')
->label(trans('admin/node.tabs.advanced_settings'))
->icon('tabler-server-cog')
->columnSpanFull()
->columns([
'default' => 2,
'sm' => 3,
'md' => 3,
'lg' => 4,
])
->schema([
ToggleButtons::make('maintenance_mode')
->label(trans('admin/node.maintenance_mode'))->inline()
->columnSpan(1)
->default(false)
->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help'))
->options([
true => trans('admin/node.enabled'),
false => trans('admin/node.disabled'),
])
->colors([
true => 'danger',
false => 'success',
]),
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label(trans('admin/node.use_for_deploy'))->inline()
->options([
true => trans('admin/node.yes'),
false => trans('admin/node.no'),
])
->colors([
true => 'success',
false => 'danger',
]),
TagsInput::make('tags')
->label(trans('admin/node.tags'))
->columnSpan(2),
TextInput::make('upload_size')
->label(trans('admin/node.upload_limit'))
->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help'))
->columnSpan(1)
->numeric()->required()
->default(256)
->minValue(1)
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'),
TextInput::make('daemon_sftp')
->columnSpan(1)
->label(trans('admin/node.sftp_port'))
->minValue(1)
->maxValue(65535)
->default(2022)
->required()
->integer(),
TextInput::make('daemon_sftp_alias')
->columnSpan(2)
->label(trans('admin/node.sftp_alias'))
->helperText(trans('admin/node.sftp_alias_help')),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_mem')
->dehydrated()
->label(trans('admin/node.memory'))->inlineLabel()->inline()
->afterStateUpdated(fn (Set $set) => $set('memory', 0))
->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('memory') == 0)
->live()
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('memory')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->label(trans('admin/node.memory_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('memory_overallocate')
->dehydratedWhenHidden()
->label(trans('admin/node.overallocate'))->inlineLabel()
->hidden(fn (Get $get) => $get('unlimited_mem'))
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_disk')
->dehydrated()
->label(trans('admin/node.disk'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('disk', 0))
->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('disk') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('disk')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.disk_limit'))->inlineLabel()
->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB')
->columnSpan(2)
->numeric()
->minValue(0)
->default(0)
->required(),
TextInput::make('disk_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_disk'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->minValue(-1)
->maxValue(100)
->default(0)
->suffix('%')
->required(),
]),
Grid::make()
->columns(6)
->columnSpanFull()
->schema([
ToggleButtons::make('unlimited_cpu')
->dehydrated()
->label(trans('admin/node.cpu'))->inlineLabel()->inline()
->live()
->afterStateUpdated(fn (Set $set) => $set('cpu', 0))
->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0))
->formatStateUsing(fn (Get $get) => $get('cpu') == 0)
->options([
true => trans('admin/node.unlimited'),
false => trans('admin/node.limited'),
])
->colors([
true => 'primary',
false => 'warning',
])
->columnSpan(2),
TextInput::make('cpu')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.cpu_limit'))->inlineLabel()
->suffix('%')
->columnSpan(2)
->numeric()
->default(0)
->minValue(0)
->required(),
TextInput::make('cpu_overallocate')
->dehydratedWhenHidden()
->hidden(fn (Get $get) => $get('unlimited_cpu'))
->label(trans('admin/node.overallocate'))->inlineLabel()
->columnSpan(2)
->numeric()
->default(0)
->minValue(-1)
->maxValue(100)
->suffix('%')
->required(),
]),
]),
];
}
protected function getRedirectUrlParameters(): array
{
return [

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@ use App\Models\UserSSHKey;
use App\Services\Helpers\LanguageService;
use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanCustomizeStaticTabs;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use DateTimeZone;
@@ -60,7 +59,6 @@ class UserResource extends Resource
{
use CanCustomizePages;
use CanCustomizeRelations;
use CanCustomizeStaticTabs;
use CanModifyForm;
use CanModifyTable;
@@ -148,339 +146,332 @@ class UserResource extends Resource
->columns(['default' => 1, 'lg' => 3, 'md' => 2])
->components([
Tabs::make()
->schema(static::getTabs())
->columnSpanFull(),
]);
}
->schema([
Tab::make('account')
->label(trans('profile.tabs.account'))
->icon('tabler-user-cog')
->columns([
'default' => 1,
'md' => 3,
'lg' => 3,
])
->schema([
TextInput::make('username')
->label(trans('admin/user.username'))
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
])
->required()
->unique()
->maxLength(255),
TextInput::make('email')
->label(trans('admin/user.email'))
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
])
->email()
->required()
->unique()
->maxLength(255),
TextInput::make('password')
->label(trans('admin/user.password'))
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
])
->hintIcon(fn ($operation) => $operation === 'create' ? 'tabler-question-mark' : null, fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
->password()
->hintAction(
Action::make('password_reset')
->label(trans('admin/user.password_reset'))
->hidden(fn (string $operation) => $operation === 'create' || config('mail.default', 'log') === 'log')
->icon('tabler-send')
->action(function (User $user) {
$status = Password::broker(Filament::getPanel('app')->getAuthPasswordBroker())->sendResetLink([
'email' => $user->email,
],
function (User $user, string $token) {
$notification = new ResetPassword($token);
$notification->url = Filament::getPanel('app')->getResetPasswordUrl($token, $user);
/** @return Tab[] */
protected static function getDefaultTabs(): array
{
return [
Tab::make('account')
->label(trans('profile.tabs.account'))
->icon('tabler-user-cog')
->columns([
'default' => 1,
'md' => 3,
'lg' => 3,
])
->schema([
TextInput::make('username')
->label(trans('admin/user.username'))
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
])
->required()
->unique()
->maxLength(255),
TextInput::make('email')
->label(trans('admin/user.email'))
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
])
->email()
->required()
->unique()
->maxLength(255),
TextInput::make('password')
->label(trans('admin/user.password'))
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
])
->hintIcon(fn ($operation) => $operation === 'create' ? 'tabler-question-mark' : null, fn ($operation) => $operation === 'create' ? trans('admin/user.password_help') : null)
->password()
->hintAction(
Action::make('password_reset')
->label(trans('admin/user.password_reset'))
->hidden(fn (string $operation) => $operation === 'create' || config('mail.default', 'log') === 'log')
->icon('tabler-send')
->action(function (User $user) {
$status = Password::broker(Filament::getPanel('app')->getAuthPasswordBroker())->sendResetLink([
'email' => $user->email,
],
function (User $user, string $token) {
$notification = new ResetPassword($token);
$notification->url = Filament::getPanel('app')->getResetPasswordUrl($token, $user);
$user->notify($notification);
$user->notify($notification);
event(new PasswordResetLinkSent($user));
},
);
event(new PasswordResetLinkSent($user));
},
);
if ($status === Password::RESET_LINK_SENT) {
Notification::make()
->title(trans('admin/user.password_reset_sent'))
->success()
->send();
} else {
Notification::make()
->title(trans('admin/user.password_reset_failed'))
->body($status)
->danger()
->send();
}
})),
TextInput::make('external_id')
->label(trans('admin/user.external_id'))
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
]),
Toggle::make('is_managed_externally')
->label(trans('admin/user.is_managed_externally'))
->hintIcon('tabler-question-mark', trans('admin/user.is_managed_externally_helper'))
->inline(false)
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
]),
Section::make(trans('profile.tabs.customization'))
->collapsible()
->columnSpanFull()
->columns(2)
->schema([
Select::make('timezone')
->label(trans('profile.timezone'))
->required()
->prefixIcon('tabler-clock-pin')
->default(fn () => config('app.timezone', 'UTC'))
->selectablePlaceholder(false)
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
->searchable(),
Select::make('language')
->label(trans('profile.language'))
->required()
->prefixIcon('tabler-flag')
->live()
->default('en')
->searchable()
->selectablePlaceholder(false)
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
FileUpload::make('avatar')
->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false)
->columnSpanFull()
->avatar()
->directory('avatars')
->disk('public')
->formatStateUsing(function (FileUpload $fileUpload, ?User $user) {
if (!$user) {
return null;
}
$path = $fileUpload->getDirectory() . '/' . $user->id . '.png';
if ($fileUpload->getDisk()->exists($path)) {
return $path;
}
})
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
if ($file instanceof TemporaryUploadedFile) {
return $file->delete();
}
if ($status === Password::RESET_LINK_SENT) {
Notification::make()
->title(trans('admin/user.password_reset_sent'))
->success()
->send();
} else {
Notification::make()
->title(trans('admin/user.password_reset_failed'))
->body($status)
->danger()
->send();
}
})),
TextInput::make('external_id')
->label(trans('admin/user.external_id'))
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
]),
Toggle::make('is_managed_externally')
->label(trans('admin/user.is_managed_externally'))
->hintIcon('tabler-question-mark', trans('admin/user.is_managed_externally_helper'))
->inline(false)
->columnSpan([
'default' => 1,
'md' => 1,
'lg' => 1,
]),
Section::make(trans('profile.tabs.customization'))
->collapsible()
->columnSpanFull()
->columns(2)
->schema([
Select::make('timezone')
->label(trans('profile.timezone'))
->required()
->prefixIcon('tabler-clock-pin')
->default(fn () => config('app.timezone', 'UTC'))
->selectablePlaceholder(false)
->options(fn () => collect(DateTimeZone::listIdentifiers())->mapWithKeys(fn ($tz) => [$tz => $tz]))
->searchable(),
Select::make('language')
->label(trans('profile.language'))
->required()
->prefixIcon('tabler-flag')
->live()
->default('en')
->searchable()
->selectablePlaceholder(false)
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages()),
FileUpload::make('avatar')
->visible(fn (?User $user, FileUpload $fileUpload) => $user ? $fileUpload->getDisk()->exists($fileUpload->getDirectory() . '/' . $user->id . '.png') : false)
->columnSpanFull()
->avatar()
->directory('avatars')
->disk('public')
->formatStateUsing(function (FileUpload $fileUpload, ?User $user) {
if (!$user) {
return null;
}
$path = $fileUpload->getDirectory() . '/' . $user->id . '.png';
if ($fileUpload->getDisk()->exists($path)) {
return $path;
}
})
->deleteUploadedFileUsing(function (FileUpload $fileUpload, $file) {
if ($file instanceof TemporaryUploadedFile) {
return $file->delete();
}
if ($fileUpload->getDisk()->exists($file)) {
return $fileUpload->getDisk()->delete($file);
}
}),
]),
Section::make(trans('profile.tabs.oauth'))
->visible(fn (?User $user) => $user)
->collapsible()
->columnSpanFull()
->schema(function (OAuthService $oauthService, ?User $user) {
if (!$user) {
return;
}
if ($fileUpload->getDisk()->exists($file)) {
return $fileUpload->getDisk()->delete($file);
}
}),
]),
Section::make(trans('profile.tabs.oauth'))
->visible(fn (?User $user) => $user)
->collapsible()
->columnSpanFull()
->schema(function (OAuthService $oauthService, ?User $user) {
if (!$user) {
return;
}
$actions = [];
foreach ($user->oauth ?? [] as $schema => $_) {
$schema = $oauthService->get($schema);
if (!$schema) {
return;
}
$id = $schema->getId();
$name = $schema->getName();
$color = $schema->getHexColor();
$color = is_string($color) ? Color::hex($color) : null;
$actions[] = Action::make("oauth_$id")
->label(trans('profile.unlink', ['name' => $name]))
->icon('tabler-unlink')
->requiresConfirmation()
->color($color)
->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
$oauthService->unlinkUser($user, $schema);
$livewire->form->fill($user->attributesToArray());
Notification::make()
->title(trans('profile.unlinked', ['name' => $name]))
->success()
->send();
});
}
if (!$actions) {
return [
TextEntry::make('no_oauth')
->state(trans('profile.no_oauth'))
->hiddenLabel(),
];
}
return [Actions::make($actions)];
}),
]),
Tab::make('roles')
->label(trans('admin/user.roles'))
->icon('tabler-users-group')
->schema([
CheckboxList::make('roles')
->hidden(fn (?User $user) => $user && $user->isRootAdmin())
->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
->dehydrated()
->label(trans('admin/user.admin_roles'))
->columnSpanFull()
->bulkToggleable(false),
CheckboxList::make('root_admin_role')
->visible(fn (?User $user) => $user && $user->isRootAdmin())
->disabled()
->options([
'root_admin' => Role::ROOT_ADMIN,
])
->descriptions([
'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
])
->formatStateUsing(fn () => ['root_admin'])
->dehydrated(false)
->label(trans('admin/user.admin_roles'))
->columnSpanFull(),
]),
Tab::make('keys')
->visible(fn (?User $user) => $user)
->label(trans('profile.tabs.keys'))
->icon('tabler-key')
->schema([
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'])
->deleteAction(function (Action $action) {
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, ?User $user) {
$items = $component->getState();
$key = $items[$arguments['item']] ?? null;
if ($key) {
$apiKey = ApiKey::find($key['id']);
if ($apiKey?->exists()) {
$apiKey->delete();
Activity::event('user:api-key.delete')
->actor(user())
->subject($user)
->subject($apiKey)
->property('identifier', $apiKey->identifier)
->log();
$actions = [];
foreach ($user->oauth ?? [] as $schema => $_) {
$schema = $oauthService->get($schema);
if (!$schema) {
return;
}
unset($items[$arguments['item']]);
$component->state($items);
$component->callAfterStateUpdated();
}
});
})
->schema([
TextEntry::make('memo')
->hiddenLabel()
->state(fn (ApiKey $key) => $key->memo),
])
->visible(fn (User $user) => $user->apiKeys()->exists()),
$id = $schema->getId();
$name = $schema->getName();
TextEntry::make('no_api_keys')
->state(trans('profile.no_api_keys'))
->hiddenLabel()
->visible(fn (User $user) => !$user->apiKeys()->exists()),
]),
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'])
->deleteAction(function (Action $action) {
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
$items = $component->getState();
$key = $items[$arguments['item']];
$color = $schema->getHexColor();
$color = is_string($color) ? Color::hex($color) : null;
$sshKey = UserSSHKey::find($key['id'] ?? null);
if ($sshKey->exists()) {
$sshKey->delete();
Activity::event('user:ssh-key.delete')
->actor(user())
->subject($user)
->subject($sshKey)
->property('fingerprint', $sshKey->fingerprint)
->log();
$actions[] = Action::make("oauth_$id")
->label(trans('profile.unlink', ['name' => $name]))
->icon('tabler-unlink')
->requiresConfirmation()
->color($color)
->action(function ($livewire) use ($oauthService, $user, $name, $schema) {
$oauthService->unlinkUser($user, $schema);
$livewire->form->fill($user->attributesToArray());
Notification::make()
->title(trans('profile.unlinked', ['name' => $name]))
->success()
->send();
});
}
unset($items[$arguments['item']]);
if (!$actions) {
return [
TextEntry::make('no_oauth')
->state(trans('profile.no_oauth'))
->hiddenLabel(),
];
}
$component->state($items);
return [Actions::make($actions)];
}),
]),
Tab::make('roles')
->label(trans('admin/user.roles'))
->icon('tabler-users-group')
->components([
CheckboxList::make('roles')
->hidden(fn (?User $user) => $user && $user->isRootAdmin())
->relationship('roles', 'name', fn (Builder $query) => $query->whereNot('id', Role::getRootAdmin()->id))
->saveRelationshipsUsing(fn (User $user, array $state) => $user->syncRoles(collect($state)->map(fn ($role) => Role::findById($role))))
->dehydrated()
->label(trans('admin/user.admin_roles'))
->columnSpanFull()
->bulkToggleable(false),
CheckboxList::make('root_admin_role')
->visible(fn (?User $user) => $user && $user->isRootAdmin())
->disabled()
->options([
'root_admin' => Role::ROOT_ADMIN,
])
->descriptions([
'root_admin' => trans('admin/role.root_admin', ['role' => Role::ROOT_ADMIN]),
])
->formatStateUsing(fn () => ['root_admin'])
->dehydrated(false)
->label(trans('admin/user.admin_roles'))
->columnSpanFull(),
]),
Tab::make('keys')
->visible(fn (?User $user) => $user)
->label(trans('profile.tabs.keys'))
->icon('tabler-key')
->schema([
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'])
->deleteAction(function (Action $action) {
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, ?User $user) {
$items = $component->getState();
$key = $items[$arguments['item']] ?? null;
$component->callAfterStateUpdated();
});
})
->schema(fn () => [
TextEntry::make('fingerprint')
->hiddenLabel()
->state(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
])
->visible(fn (User $user) => $user->sshKeys()->exists()),
if ($key) {
$apiKey = ApiKey::find($key['id']);
if ($apiKey?->exists()) {
$apiKey->delete();
TextEntry::make('no_ssh_keys')
->state(trans('profile.no_ssh_keys'))
->hiddenLabel()
->visible(fn (User $user) => !$user->sshKeys()->exists()),
]),
]),
Tab::make('activity')
->visible(fn (?User $user) => $user)
->disabledOn('create')
->label(trans('profile.tabs.activity'))
->icon('tabler-history')
->schema([
Repeater::make('activity')
->hiddenLabel()
->inlineLabel(false)
->deletable(false)
->addable(false)
->relationship(null, function (Builder $query) {
$query->orderBy('timestamp', 'desc');
})
->schema([
TextEntry::make('log')
->hiddenLabel()
->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
]),
]),
];
Activity::event('user:api-key.delete')
->actor(user())
->subject($user)
->subject($apiKey)
->property('identifier', $apiKey->identifier)
->log();
}
unset($items[$arguments['item']]);
$component->state($items);
$component->callAfterStateUpdated();
}
});
})
->schema([
TextEntry::make('memo')
->hiddenLabel()
->state(fn (ApiKey $key) => $key->memo),
])
->visible(fn (User $user) => $user->apiKeys()->exists()),
TextEntry::make('no_api_keys')
->state(trans('profile.no_api_keys'))
->hiddenLabel()
->visible(fn (User $user) => !$user->apiKeys()->exists()),
]),
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'])
->deleteAction(function (Action $action) {
$action->requiresConfirmation()->action(function (array $arguments, Repeater $component, User $user) {
$items = $component->getState();
$key = $items[$arguments['item']];
$sshKey = UserSSHKey::find($key['id'] ?? null);
if ($sshKey->exists()) {
$sshKey->delete();
Activity::event('user:ssh-key.delete')
->actor(user())
->subject($user)
->subject($sshKey)
->property('fingerprint', $sshKey->fingerprint)
->log();
}
unset($items[$arguments['item']]);
$component->state($items);
$component->callAfterStateUpdated();
});
})
->schema(fn () => [
TextEntry::make('fingerprint')
->hiddenLabel()
->state(fn (UserSSHKey $key) => "SHA256:{$key->fingerprint}"),
])
->visible(fn (User $user) => $user->sshKeys()->exists()),
TextEntry::make('no_ssh_keys')
->state(trans('profile.no_ssh_keys'))
->hiddenLabel()
->visible(fn (User $user) => !$user->sshKeys()->exists()),
]),
]),
Tab::make('activity')
->visible(fn (?User $user) => $user)
->disabledOn('create')
->label(trans('profile.tabs.activity'))
->icon('tabler-history')
->schema([
Repeater::make('activity')
->hiddenLabel()
->inlineLabel(false)
->deletable(false)
->addable(false)
->relationship(null, function (Builder $query) {
$query->orderBy('timestamp', 'desc');
})
->schema([
TextEntry::make('log')
->hiddenLabel()
->state(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
]),
]),
])->columnSpanFull(),
]);
}
/** @return class-string<RelationManager>[] */

View File

@@ -3,24 +3,24 @@
namespace App\Filament\Components\Actions;
use App\Console\Commands\Egg\UpdateEggIndexCommand;
use App\Jobs\InstallEgg;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggImporterService;
use Closure;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\Width;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Str;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImportEggAction extends Action
@@ -42,31 +42,10 @@ class ImportEggAction extends Action
$this->iconSize(IconSize::ExtraLarge);
$this->modalWidth(Width::ScreenExtraLarge);
$this->authorize(fn () => user()?->can('import egg'));
$this->action(function (array $data, EggImporterService $eggImportService): void {
$gitHubEggs = array_get($this->data, 'eggs', []);
$eggs = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
if ($gitHubEggs) {
foreach ($gitHubEggs as $category => $sortedEggs) {
foreach ($sortedEggs as $downloadUrl) {
InstallEgg::dispatch($downloadUrl);
}
}
Notification::make()
->title(trans('installer.egg.background_install_started'))
->body(trans('installer.egg.background_install_description', ['count' => array_sum(array_map('count', $gitHubEggs))]))
->success()
->persistent()
->send();
}
if (empty($eggs)) {
return;
}
@@ -110,20 +89,18 @@ class ImportEggAction extends Action
}
}
$bodyParts = collect([
$success->isNotEmpty() ? trans('admin/egg.import.imported_eggs', ['eggs' => $success->join(', ')]) : null,
$failed->isNotEmpty() ? trans('admin/egg.import.failed_import_eggs', ['eggs' => $failed->join(', ')]) : null,
])->filter();
if ($bodyParts->isNotEmpty()) {
if ($failed->count() > 0) {
Notification::make()
->title(trans('admin/egg.import.import_result', [
'success' => $success->count(),
'failed' => $failed->count(),
'total' => $success->count() + $failed->count(),
]))
->body($bodyParts->join(' | '))
->status($failed->isEmpty() ? 'success' : ($success->isEmpty() ? 'danger' : 'warning'))
->title(trans('admin/egg.import.import_failed'))
->body($failed->join(', '))
->danger()
->send();
}
if ($success->count() > 0) {
Notification::make()
->title(trans('admin/egg.import.import_success'))
->body($success->join(', '))
->success()
->send();
}
});
@@ -136,7 +113,6 @@ class ImportEggAction extends Action
Tabs::make('Tabs')
->contained(false)
->tabs([
$this->importEggsFromGitHub(),
Tab::make('file')
->label(trans('admin/egg.import.file'))
->icon('tabler-file-upload')
@@ -154,6 +130,30 @@ class ImportEggAction extends Action
->label(trans('admin/egg.import.url'))
->icon('tabler-world-upload')
->schema([
Select::make('github')
->label(trans('admin/egg.import.github'))
->options(fn () => cache('eggs.index'))
->selectablePlaceholder(false)
->searchable()
->preload()
->live()
->hintAction(
Action::make('refresh')
->iconButton()
->icon('tabler-refresh')
->tooltip(trans('admin/egg.import.refresh'))
->action(function () {
Artisan::call(UpdateEggIndexCommand::class);
})
)
->afterStateUpdated(function ($state, Set $set, Get $get) use ($isMultiple) {
if ($state) {
$urls = $isMultiple ? $get('urls') : [];
$urls[Str::uuid()->toString()] = ['url' => $state];
$set('urls', $urls);
$set('github', null);
}
}),
Repeater::make('urls')
->hiddenLabel()
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->beforeLast('.')->headline())
@@ -179,49 +179,4 @@ class ImportEggAction extends Action
return $this;
}
public function importEggsFromGitHub(): Tab
{
if (!cache()->get('eggs.index')) {
Artisan::call(UpdateEggIndexCommand::class);
}
$eggs = cache()->get('eggs.index', []);
$categories = array_keys($eggs);
$tabs = array_map(function (string $label) use ($eggs) {
$id = str_slug($label, '_');
$eggCount = count($eggs[$label]);
return Tab::make($id)
->label($label)
->badge($eggCount)
->schema([
CheckboxList::make("eggs.$id")
->hiddenLabel()
->options(fn () => array_sort($eggs[$label]))
->searchable($eggCount > 0)
->bulkToggleable($eggCount > 0)
->columns(4),
]);
}, $categories);
if (empty($tabs)) {
$tabs[] = Tab::make('no_eggs')
->label(trans('installer.egg.no_eggs'))
->schema([
TextEntry::make('no_eggs')
->hiddenLabel()
->state(trans('installer.egg.exceptions.no_eggs')),
]);
}
return Tab::make('github')
->label(trans('admin/egg.import.github'))
->icon('tabler-brand-github')
->columnSpanFull()
->schema([
Tabs::make('egg_tabs')
->tabs($tabs),
]);
}
}

View File

@@ -47,8 +47,8 @@ class UpdateEggAction extends Action
cache()->forget("eggs.$egg->uuid.update");
} catch (Exception $exception) {
Notification::make()
->title(trans('admin/egg.update_failed', ['egg' => $egg->name]))
->body(trans('admin/egg.update_error', ['error' => $exception->getMessage()]))
->title(trans('admin/egg.update_failed'))
->body($exception->getMessage())
->danger()
->send();
@@ -58,8 +58,8 @@ class UpdateEggAction extends Action
}
Notification::make()
->title(trans('admin/egg.update_success', ['egg' => $egg->name]))
->body(trans('admin/egg.updated_from', ['url' => $egg->update_url]))
->title(trans_choice('admin/egg.updated', 1))
->body($egg->name)
->success()
->send();
});

View File

@@ -47,40 +47,39 @@ class UpdateEggBulkAction extends BulkAction
return;
}
$successEggs = collect();
$failedEggs = collect();
$skippedEggs = collect();
$success = 0;
$failed = 0;
$skipped = 0;
/** @var Egg $egg */
foreach ($records as $egg) {
if ($egg->update_url === null) {
$skippedEggs->push($egg->name);
$skipped++;
continue;
}
try {
$eggImporterService->fromUrl($egg->update_url, $egg);
$successEggs->push($egg->name);
$success++;
cache()->forget("eggs.$egg->uuid.update");
} catch (Exception $exception) {
$failedEggs->push($egg->name);
$failed++;
report($exception);
}
}
$bodyParts = collect([
$successEggs->isNotEmpty() ? trans('admin/egg.updated_eggs', ['eggs' => $successEggs->join(', ')]) : null,
$failedEggs->isNotEmpty() ? trans('admin/egg.failed_eggs', ['eggs' => $failedEggs->join(', ')]) : null,
$skippedEggs->isNotEmpty() ? trans('admin/egg.skipped_eggs', ['eggs' => $skippedEggs->join(', ')]) : null,
])->filter();
Notification::make()
->title(trans_choice('admin/egg.updated', 2, ['count' => $successEggs->count(), 'total' => $records->count()]))
->body($bodyParts->join(' | '))
->status($failedEggs->isNotEmpty() ? 'warning' : 'success')
->title(trans_choice('admin/egg.updated', 2, ['count' => $success, 'total' => $records->count()]))
->body(
collect([
$failed > 0 ? trans('admin/egg.updated_failed', ['count' => $failed]) : null,
$skipped > 0 ? trans('admin/egg.updated_skipped', ['count' => $skipped]) : null,
])->filter()->join(' ')
)
->status($failed > 0 ? 'warning' : 'success')
->persistent()
->send();
});

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,6 @@ use App\Services\Helpers\LanguageService;
use App\Services\Users\UserCreationService;
use App\Traits\CheckMigrationsTrait;
use App\Traits\EnvironmentWriterTrait;
use App\Traits\Filament\CanCustomizeSteps;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
@@ -27,7 +26,6 @@ use Filament\Pages\SimplePage;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Wizard;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Filament\Support\Exceptions\Halt;
@@ -41,7 +39,6 @@ use Illuminate\Support\HtmlString;
*/
class PanelInstaller extends SimplePage implements HasForms
{
use CanCustomizeSteps;
use CheckMigrationsTrait;
use EnvironmentWriterTrait;
use InteractsWithForms;
@@ -73,7 +70,9 @@ class PanelInstaller extends SimplePage implements HasForms
$this->form->fill();
}
/** @return Component[] */
/** @return Component[]
* @throws Exception
*/
protected function getFormSchema(): array
{
return [
@@ -81,9 +80,21 @@ class PanelInstaller extends SimplePage implements HasForms
->schema([
$this->getLanguageComponent(),
]),
Wizard::make($this->getSteps())
Wizard::make([
RequirementsStep::make(),
EnvironmentStep::make($this),
DatabaseStep::make($this),
EggSelectionStep::make(),
CacheStep::make($this),
QueueStep::make($this),
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"
@@ -97,24 +108,6 @@ class PanelInstaller extends SimplePage implements HasForms
];
}
/**
* @return Step[]
*
* @throws Exception
*/
protected function getDefaultSteps(): array
{
return [
RequirementsStep::make(),
EnvironmentStep::make($this),
DatabaseStep::make($this),
EggSelectionStep::make(),
CacheStep::make($this),
QueueStep::make($this),
SessionStep::make(),
];
}
protected function getLanguageComponent(): Component
{
return Select::make('language')

View File

@@ -168,33 +168,33 @@ class ActivityLog extends Model implements HasIcon, HasLabel
return user()?->can('seeIps activityLog') ? $this->ip : null;
}
public function htmlable(): string
{
$user = $this->actor;
if (!$user instanceof User) {
$user = new User([
'email' => 'system@pelican.dev',
'username' => 'system',
]);
}
$avatarUrl = Filament::getUserAvatarUrl($user);
$username = str($user->username)->stripTags();
$ip = $this->getIp();
$ip = $ip ? $ip . ' — ' : '';
return "
<div style='display: flex; align-items: center;'>
<img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px; border-radius: 50%;' />
<div>
<p>$username$this->event</p>
<p>{$this->getLabel()}</p>
<p>$ip<span title='{$this->timestamp->format('M j, Y g:ia')}'>{$this->timestamp->diffForHumans()}</span></p>
</div>
</div>
";
}
// public function htmlable(): string
// {
// $user = $this->actor;
// if (!$user instanceof User) {
// $user = new User([
// 'email' => 'system@pelican.dev',
// 'username' => 'system',
// ]);
// }
//
// $avatarUrl = Filament::getUserAvatarUrl($user);
// $username = str($user->username)->stripTags();
// $ip = $this->getIp();
// $ip = $ip ? $ip . ' — ' : '';
//
// return "
// <div style='display: flex; align-items: center;'>
// <img width='50px' height='50px' src='{$avatarUrl}' style='margin-right: 15px; border-radius: 50%;' />
//
// <div>
// <p>$username — $this->event</p>
// <p>{$this->getLabel()}</p>
// <p>$ip<span title='{$this->timestamp->format('M j, Y g:ia')}'>{$this->timestamp->diffForHumans()}</span></p>
// </div>
// </div>
// ";
// }
/**
* @return array<string, string>

View File

@@ -3,69 +3,34 @@
namespace App\Policies;
use App\Enums\SubuserPermission;
use App\Models\Server;
use App\Models\User;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Model;
class AllocationPolicy
{
use DefaultAdminPolicies {
viewAny as adminViewAny;
view as adminView;
create as adminCreate;
update as adminUpdate;
delete as adminDelete;
deleteAny as adminDeleteAny;
}
protected string $modelName = 'allocation';
public function viewAny(User $user): bool
{
/** @var ?Server $server */
$server = Filament::getTenant();
return $server ? $user->can(SubuserPermission::AllocationRead, $server) : $this->adminViewAny($user);
return $user->can(SubuserPermission::AllocationRead, Filament::getTenant());
}
public function view(User $user, Model $model): bool
public function view(User $user, Model $record): bool
{
/** @var ?Server $server */
$server = Filament::getTenant();
return $server ? $user->can(SubuserPermission::AllocationRead, $server) : $this->adminView($user, $model);
return $user->can(SubuserPermission::AllocationRead, Filament::getTenant());
}
public function create(User $user): bool
{
/** @var ?Server $server */
$server = Filament::getTenant();
return $server ? $user->can(SubuserPermission::AllocationCreate, $server) : $this->adminCreate($user);
return $user->can(SubuserPermission::AllocationCreate, Filament::getTenant());
}
public function update(User $user, Model $model): bool
public function edit(User $user, Model $record): bool
{
/** @var ?Server $server */
$server = Filament::getTenant();
return $server ? $user->can(SubuserPermission::AllocationUpdate, $server) : $this->adminUpdate($user, $model);
return $user->can(SubuserPermission::AllocationUpdate, Filament::getTenant());
}
public function delete(User $user, Model $model): bool
public function delete(User $user, Model $record): bool
{
/** @var ?Server $server */
$server = Filament::getTenant();
return $server ? $user->can(SubuserPermission::AllocationDelete, $server) : $this->adminDelete($user, $model);
}
public function deleteAny(User $user): bool
{
/** @var ?Server $server */
$server = Filament::getTenant();
return $server ? $user->can(SubuserPermission::AllocationDelete, $server) : $this->adminDeleteAny($user);
return $user->can(SubuserPermission::AllocationDelete, Filament::getTenant());
}
}

View File

@@ -14,7 +14,7 @@ class BackupPolicy
return $user->can(SubuserPermission::BackupRead, Filament::getTenant());
}
public function view(User $user, Model $model): bool
public function view(User $user, Model $record): bool
{
return $user->can(SubuserPermission::BackupRead, Filament::getTenant());
}
@@ -24,7 +24,7 @@ class BackupPolicy
return $user->can(SubuserPermission::BackupCreate, Filament::getTenant());
}
public function delete(User $user, Model $model): bool
public function delete(User $user, Model $record): bool
{
return $user->can(SubuserPermission::BackupDelete, Filament::getTenant());
}

View File

@@ -14,7 +14,7 @@ class DatabasePolicy
return $user->can(SubuserPermission::DatabaseRead, Filament::getTenant());
}
public function view(User $user, Model $model): bool
public function view(User $user, Model $record): bool
{
return $user->can(SubuserPermission::DatabaseRead, Filament::getTenant());
}
@@ -24,12 +24,12 @@ class DatabasePolicy
return $user->can(SubuserPermission::DatabaseCreate, Filament::getTenant());
}
public function update(User $user, Model $model): bool
public function edit(User $user, Model $record): bool
{
return $user->can(SubuserPermission::DatabaseUpdate, Filament::getTenant());
}
public function delete(User $user, Model $model): bool
public function delete(User $user, Model $record): bool
{
return $user->can(SubuserPermission::DatabaseDelete, Filament::getTenant());
}

View File

@@ -14,7 +14,7 @@ class FilePolicy
return $user->can(SubuserPermission::FileRead, Filament::getTenant());
}
public function view(User $user, Model $model): bool
public function view(User $user, Model $record): bool
{
return $user->can(SubuserPermission::FileReadContent, Filament::getTenant());
}
@@ -24,12 +24,12 @@ class FilePolicy
return $user->can(SubuserPermission::FileCreate, Filament::getTenant());
}
public function update(User $user, Model $model): bool
public function edit(User $user, Model $record): bool
{
return $user->can(SubuserPermission::FileUpdate, Filament::getTenant());
}
public function delete(User $user, Model $model): bool
public function delete(User $user, Model $record): bool
{
return $user->can(SubuserPermission::FileDelete, Filament::getTenant());
}

View File

@@ -14,7 +14,7 @@ class SchedulePolicy
return $user->can(SubuserPermission::ScheduleRead, Filament::getTenant());
}
public function view(User $user, Model $model): bool
public function view(User $user, Model $record): bool
{
return $user->can(SubuserPermission::ScheduleRead, Filament::getTenant());
}
@@ -24,12 +24,12 @@ class SchedulePolicy
return $user->can(SubuserPermission::ScheduleCreate, Filament::getTenant());
}
public function update(User $user, Model $model): bool
public function edit(User $user, Model $record): bool
{
return $user->can(SubuserPermission::ScheduleUpdate, Filament::getTenant());
}
public function delete(User $user, Model $model): bool
public function delete(User $user, Model $record): bool
{
return $user->can(SubuserPermission::ScheduleDelete, Filament::getTenant());
}

View File

@@ -14,7 +14,7 @@ class SubuserPolicy
return $user->can(SubuserPermission::UserRead, Filament::getTenant());
}
public function view(User $user, Model $model): bool
public function view(User $user, Model $record): bool
{
return $user->can(SubuserPermission::UserRead, Filament::getTenant());
}
@@ -24,12 +24,12 @@ class SubuserPolicy
return $user->can(SubuserPermission::UserCreate, Filament::getTenant());
}
public function update(User $user, Model $model): bool
public function edit(User $user, Model $record): bool
{
return $user->can(SubuserPermission::UserUpdate, Filament::getTenant());
}
public function delete(User $user, Model $model): bool
public function delete(User $user, Model $record): bool
{
return $user->can(SubuserPermission::UserDelete, Filament::getTenant());
}

View File

@@ -3,7 +3,6 @@
namespace App\Services\Servers;
use App\Models\Allocation;
use App\Models\Backup;
use App\Models\Node;
use App\Models\Server;
use App\Models\ServerTransfer;
@@ -24,19 +23,11 @@ class TransferServerService
private NodeJWTService $nodeJWTService,
) {}
/**
* @param string[] $backup_uuids
*/
private function notify(ServerTransfer $transfer, UnencryptedToken $token, array $backup_uuids = []): void
private function notify(ServerTransfer $transfer, UnencryptedToken $token): void
{
$backups = [];
if (config('backups.default') === Backup::ADAPTER_DAEMON) {
$backups = $backup_uuids;
}
Http::daemon($transfer->oldNode)->post("/api/servers/{$transfer->server->uuid}/transfer", [
'url' => $transfer->newNode->getConnectionAddress() . '/api/transfers',
'token' => 'Bearer ' . $token->toString(),
'backups' => $backups,
'server' => [
'uuid' => $transfer->server->uuid,
'start_on_completion' => false,
@@ -48,11 +39,10 @@ class TransferServerService
* Starts a transfer of a server to a new node.
*
* @param int[] $additional_allocations
* @param string[] $backup_uuid
*
* @throws Throwable
*/
public function handle(Server $server, int $node_id, ?int $allocation_id = null, ?array $additional_allocations = [], ?array $backup_uuid = []): bool
public function handle(Server $server, int $node_id, ?int $allocation_id = null, ?array $additional_allocations = []): bool
{
$additional_allocations = array_map(intval(...), $additional_allocations);
@@ -103,7 +93,7 @@ class TransferServerService
->handle($transfer->newNode, $server->uuid, 'sha256');
// Notify the source node of the pending outgoing transfer.
$this->notify($transfer, $token, $backup_uuid);
$this->notify($transfer, $token);
return true;
}

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Traits\Filament;
use App\Enums\TabPosition;
use Filament\Schemas\Components\Tabs\Tab;
trait CanCustomizeStaticTabs
{
/** @var array<string, Tab[]> */
protected static array $customTabs = [];
public static function registerCustomTabs(TabPosition $position, Tab ...$customTabs): void
{
static::$customTabs[$position->value] = array_merge(static::$customTabs[$position->value] ?? [], $customTabs);
}
/** @return Tab[] */
protected static function getDefaultTabs(): array
{
return [];
}
/** @return Tab[] */
protected static function getTabs(): array
{
return array_merge(
static::$customTabs[TabPosition::Before->value] ?? [],
static::getDefaultTabs(),
static::$customTabs[TabPosition::After->value] ?? []
);
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Traits\Filament;
use App\Enums\StepPosition;
use Filament\Schemas\Components\Wizard\Step;
trait CanCustomizeSteps
{
/** @var Step[] */
protected static array $customSteps = [];
public static function registerCustomSteps(StepPosition $position, Step ...$customSteps): void
{
static::$customSteps[$position->value] = array_merge(static::$customSteps[$position->value] ?? [], $customSteps);
}
/** @return Step[] */
protected function getDefaultSteps(): array
{
return [];
}
/** @return Step[] */
protected function getSteps(): array
{
return array_merge(
static::$customSteps[StepPosition::Before->value] ?? [],
$this->getDefaultSteps(),
static::$customSteps[StepPosition::After->value] ?? []
);
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace App\Traits\Filament;
use App\Enums\TabPosition;
use Filament\Schemas\Components\Tabs\Tab;
trait CanCustomizeTabs
{
/** @var array<string, Tab[]> */
protected static array $customTabs = [];
public static function registerCustomTabs(TabPosition $position, Tab ...$customTabs): void
{
static::$customTabs[$position->value] = array_merge(static::$customTabs[$position->value] ?? [], $customTabs);
}
/** @return Tab[] */
protected function getDefaultTabs(): array
{
return [];
}
/** @return Tab[] */
protected function getTabs(): array
{
return array_merge(
static::$customTabs[TabPosition::Before->value] ?? [],
$this->getDefaultTabs(),
static::$customTabs[TabPosition::After->value] ?? []
);
}
}

View File

@@ -9,20 +9,20 @@
"ext-mbstring": "*",
"ext-pdo": "*",
"ext-zip": "*",
"aws/aws-sdk-php": "^3.369",
"aws/aws-sdk-php": "^3.356",
"calebporzio/sushi": "^2.5",
"dedoc/scramble": "^0.13",
"filament/filament": "^4.5",
"dedoc/scramble": "^0.12.10",
"filament/filament": "~4.0",
"gboquizosanchez/filament-log-viewer": "^2.1",
"guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.47",
"laravel/helpers": "^1.8",
"laravel/framework": "^12.37",
"laravel/helpers": "^1.7",
"laravel/sanctum": "^4.2",
"laravel/socialite": "^5.24",
"laravel/socialite": "^5.23",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"lcobucci/jwt": "^5.6",
"league/flysystem-aws-s3-v3": "^3.30",
"lcobucci/jwt": "^5.5",
"league/flysystem-aws-s3-v3": "^3.29",
"league/flysystem-memory": "^3.29",
"phiki/phiki": "^2.0",
"phpseclib/phpseclib": "~3.0.18",
@@ -32,11 +32,11 @@
"socialiteproviders/authentik": "^5.2",
"socialiteproviders/discord": "^4.2",
"socialiteproviders/steam": "^4.3",
"spatie/laravel-data": "^4.18",
"spatie/laravel-data": "^4.17",
"spatie/laravel-fractal": "^6.3",
"spatie/laravel-health": "^1.34",
"spatie/laravel-permission": "^6.24",
"spatie/laravel-query-builder": "^6.4",
"spatie/laravel-permission": "^6.21",
"spatie/laravel-query-builder": "^6.3",
"spatie/temporary-directory": "^2.3",
"symfony/http-client": "^7.2",
"symfony/mailgun-mailer": "^7.2",
@@ -98,4 +98,4 @@
},
"minimum-stability": "stable",
"prefer-stable": true
}
}

665
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,8 +2,8 @@
# check for .env file or symlink and generate app keys if missing
if [ -f /var/www/html/.env ]; then
echo "external vars exist."
# load specific env vars from .env used in the entrypoint and they are not already set
for VAR in "APP_KEY" "APP_INSTALLED" "DB_CONNECTION" "DB_HOST" "DB_PORT"; do if ! (printenv | grep -q ${VAR}); then export $(grep ${VAR} .env | grep -ve "^#"); fi; done
# load env vars from .env
export $(grep -v '^#' .env | xargs)
else
echo "external vars don't exist."
# webroot .env is symlinked to this path
@@ -25,7 +25,7 @@ else
fi
# create directories for volumes
mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /pelican-data/storage/icons /var/www/html/storage/logs/supervisord 2>/dev/null
mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /var/www/html/storage/logs/supervisord 2>/dev/null
# if the app is installed then we need to run migrations on start. New installs will run migrations when you run the installer.
if [ "${APP_INSTALLED}" == "true" ]; then

View File

@@ -41,7 +41,7 @@ stdout_logfile_maxbytes=0
redirect_stderr=true
[program:supercronic]
command=supercronic -overlapping /etc/crontabs/crontab
command=supercronic -overlapping /etc/supercronic/crontab
autostart=true
autorestart=true
redirect_stderr=true

View File

@@ -17,7 +17,6 @@ return [
'intro-update-available' => [
'heading' => 'تحديث متاح',
'content' => ':latestVersion متوفر الآن! اقرأ وثائقنا لتحديث اللوحة الخاصة بك.',
'button_changelog' => 'ما الجديد؟',
],
'intro-no-update' => [
'heading' => 'لوحتك محدثة',

94
lang/ar-SA/admin/egg.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
return [
'nav_title' => 'البيوض',
'model_label' => 'البيضة',
'model_label_plural' => 'البيوض',
'tabs' => [
'configuration' => 'الإعدادات',
'process_management' => 'إدارة العمليات',
'egg_variables' => 'متغيرات البيضة',
'install_script' => 'برنامج التثبيت',
],
'import' => [
'file' => 'ملف',
'url' => 'رابط',
'egg_help' => 'يجب أن يكون هذا الملف الخام بصيغة .json (مثل egg-minecraft.json)',
'url_help' => 'يجب أن تشير الروابط مباشرة إلى ملف .json الخام',
'add_url' => 'رابط جديد',
'import_failed' => 'فشل الاستيراد',
'import_success' => 'تم الاستيراد بنجاح',
'github' => 'أضف من جيت هاب',
'refresh' => 'تحديث',
],
'export' => [
'modal' => 'كيف ترغب في تصدير :egg ؟',
'as' => 'ك.:format',
],
'in_use' => 'قيد الاستخدام',
'servers' => 'الخوادم',
'name' => 'الاسم',
'egg_uuid' => 'UUID البيضة',
'egg_id' => 'معرف البيضة',
'name_help' => 'اسم بسيط وسهل القراءة يُستخدم كمعرف لهذه البيضة.',
'author' => 'المؤلف',
'uuid_help' => 'المعرف الفريد عالميًا لهذه البيضة الذي يستخدمه Wings كمعرف.',
'author_help' => 'مؤلف هذا الإصدار من البيضة.',
'author_help_edit' => 'مؤلف هذا الإصدار من البيضة. تحميل إعدادات جديدة من مؤلف مختلف سيؤدي إلى تغييره.',
'description' => 'الوصف',
'description_help' => 'وصف لهذه البيضة سيظهر عبر اللوحة عند الحاجة.',
'startup' => 'أمر بدء التشغيل',
'startup_help' => 'أمر بدء التشغيل الافتراضي الذي سيتم استخدامه للخوادم الجديدة التي تستخدم هذه البيضة.',
'file_denylist' => 'قائمة حظر الملفات',
'file_denylist_help' => 'قائمة بالملفات التي لا يُسمح للمستخدم النهائي بتحريرها.',
'features' => 'الميزات',
'force_ip' => 'فرض عنوان IP الصادر',
'force_ip_help' => 'يجبر كل حركة المرور الصادرة على أن يكون عنوان المصدر هو عنوان التخصيص الأساسي للخادم. مطلوب لبعض الألعاب للعمل بشكل صحيح عندما تحتوي العقدة على عناوين IP عامة متعددة. عند تفعيل هذا الخيار، سيتم تعطيل الشبكات الداخلية لأي خوادم تستخدم هذه البيضة، مما يؤدي إلى عدم قدرتها على الوصول داخليًا إلى الخوادم الأخرى على نفس العقدة.',
'tags' => 'الوسوم',
'update_url' => 'رابط التحديث',
'update_url_help' => 'يجب أن تشير الروابط مباشرة إلى ملف .json الخام',
'add_image' => 'إضافة صورة Docker',
'docker_images' => 'صور Docker',
'docker_name' => 'اسم الصورة',
'docker_uri' => 'رابط الصورة',
'docker_help' => 'صور Docker المتاحة للخوادم التي تستخدم هذه البيضة.',
'stop_command' => 'أمر الإيقاف',
'stop_command_help' => 'الأمر الذي يجب إرساله إلى عمليات الخادم لإيقافها بشكل آمن. إذا كنت بحاجة إلى إرسال SIGINT، أدخل ^C هنا.',
'copy_from' => 'نسخ الإعدادات من',
'copy_from_help' => 'إذا كنت ترغب في استخدام الإعدادات الافتراضية لبيضة أخرى، حددها من القائمة أعلاه.',
'none' => 'لا شيء',
'start_config' => 'إعدادات بدء التشغيل',
'start_config_help' => 'قائمة القيم التي يجب أن يبحث عنها Daemon عند تشغيل الخادم لتحديد اكتمال التشغيل.',
'config_files' => 'ملفات الإعدادات',
'config_files_help' => 'يجب أن يكون هذا تمثيل JSON لملفات الإعدادات التي سيتم تعديلها وأي أجزاء يجب تغييرها.',
'log_config' => 'إعدادات السجلات',
'log_config_help' => 'يجب أن يكون هذا تمثيل JSON لمواقع ملفات السجلات وما إذا كان يجب على Daemon إنشاء سجلات مخصصة أم لا.',
'environment_variable' => 'المتغير البيئي',
'default_value' => 'القيمة الافتراضية',
'user_permissions' => 'أذونات المستخدم',
'viewable' => 'مرئي',
'editable' => 'قابل للتعديل',
'rules' => 'القواعد',
'add_new_variable' => 'إضافة متغير جديد',
'error_unique' => 'يوجد متغير بهذا الاسم بالفعل.',
'error_required' => 'حقل المتغير البيئي مطلوب.',
'error_reserved' => 'هذا المتغير البيئي محجوز ولا يمكن استخدامه.',
'script_from' => 'المصدر البرمجي',
'script_container' => 'حاوية السكريبت',
'script_entry' => 'مدخل السكريبت',
'script_install' => 'برنامج التثبيت',
'no_eggs' => 'لا توجد بيوض',
'no_servers' => 'لا توجد خوادم',
'no_servers_help' => 'لم يتم تعيين أي خوادم لهذه البيضة.',
'update' => 'تحديث|تحديث المحدد',
'updated' => 'تم تحديث البيضة|تم تحديث :count/: بيضة',
'updated_failed' => ':count فشلت',
'update_question' => 'هل أنت متأكد من أنك تريد تحديث هذه البيضة؟|هل أنت متأكد من أنك تريد تحديث البيض المحدد؟',
'update_description' => 'إذا قمت بأي تغييرات على البيضة فسيتم استبدالها!|إذا قمت بأي تغييرات على البيوض فسيتم استبدالها!',
'no_updates' => 'لا توجد تحديثات متوفرة للبيوض المحددة',
];

View File

@@ -0,0 +1,15 @@
<?php
return [
'model_label' => 'الجدول الزمني',
'model_label_plural' => 'الجدول الزمني',
'import' => [
'file' => 'ملف',
'url' => 'عنوان URL',
'schedule_help' => 'يجب أن يكون هذا الملف الخام بصيغة .json (مثل schedule-daily-restart.json)',
'url_help' => 'يجب أن تشير الروابط مباشرة إلى ملف .json الخام',
'add_url' => 'عنوان URL جديد',
'import_failed' => 'فشل الاستيراد',
'import_success' => 'نجح الاستيراد',
],
];

View File

@@ -13,10 +13,6 @@ return [
'ports' => 'المنافذ',
'alias' => 'الاسم المستعار',
'alias_helper' => 'اسم عرض اختياري لمساعدتك على تذكره.',
'locked' => 'مقفل؟',
'locked_helper' => 'لن يتمكن المستخدمون من حذف المخصصات المقفلة',
'lock' => 'قفل',
'unlock' => 'فتح القفل',
'name' => 'الاسم',
'external_id' => 'المعرف الخارجي',
'owner' => 'المالك',
@@ -30,9 +26,7 @@ return [
'already_primary' => 'أساسي بالفعل',
'make_primary' => 'تعيين كـ أساسي',
'startup_cmd' => 'أمر بدء التشغيل',
'startup_name' => 'اسم بدء التشغيل',
'default_startup' => 'أمر بدء التشغيل الافتراضي',
'startup_placeholder' => 'أدخل أمر بدء تشغيل مخصص',
'variables' => 'المتغيرات',
'resource_limits' => 'حدود الموارد',
'cpu' => 'المعالج',

View File

@@ -0,0 +1,153 @@
<?php
return [
'title' => 'الإعدادات',
'save_success' => 'تم حفظ الإعدادات',
'save_failed' => 'فشل في حفظ الإعدادات',
'navigation' => [
'general' => 'عام',
'captcha' => 'كابتشا',
'mail' => 'البريد',
'backup' => 'النسخ الاحتياطي',
'oauth' => 'OAuth',
'misc' => 'متفرقات',
],
'general' => [
'app_name' => 'اسم التطبيق',
'app_logo' => 'شعار التطبيق',
'app_logo_help' => 'يجب وضع الشعار في المجلد العام الموجود في المجلد الجذري للوحة التحكم، اتركه فارغاً لاستخدام اسم التطبيق بدلاً من ذلك.',
'app_favicon' => 'أيقونة التطبيق',
'app_favicon_help' => 'يجب وضع الفافيكون في المجلد العام الموجود في المجلد الجذري للوحة التحكم.',
'debug_mode' => 'وضع التصحيح',
'navigation' => 'التنقل',
'sidebar' => 'الشريط الجانبي',
'topbar' => 'الشريط العلوي',
'unit_prefix' => 'بادئة الوحدة',
'decimal_prefix' => 'البادئة العشرية (MB/GB)',
'binary_prefix' => 'البادئة الثنائية (MiB/GiB)',
'2fa_requirement' => 'متطلب المصادقة الثنائية',
'not_required' => 'غير مطلوب',
'admins_only' => 'مطلوب للمسؤولين فقط',
'all_users' => 'مطلوب لجميع المستخدمين',
'trusted_proxies' => 'الوكلاء الموثوق بهم',
'trusted_proxies_help' => 'عنوان IP جديد أو نطاق IP',
'clear' => 'مسح',
'set_to_cf' => 'تعيين إلى عناوين IP الخاصة بـ Cloudflare',
'display_width' => 'عرض الشاشة',
'avatar_provider' => 'مزود الصورة الرمزية',
'uploadable_avatars' => 'السماح للمستخدمين برفع صورهم الخاصة؟',
],
'captcha' => [
'enable' => 'تفعيل',
'disable' => 'تعطيل',
'info_label' => 'معلومات',
'info' => 'يمكنك إنشاء المفاتيح في <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">لوحة تحكم Cloudflare</a></u>، مما يتطلب حساب Cloudflare.',
'site_key' => 'مفتاح الموقع',
'secret_key' => 'المفتاح السري',
'verify' => 'التحقق من النطاق؟',
],
'mail' => [
'mail_driver' => 'مشغل البريد',
'test_mail' => 'إرسال بريد تجريبي',
'test_mail_sent' => 'تم إرسال البريد التجريبي',
'test_mail_failed' => 'فشل إرسال البريد التجريبي',
'from_settings' => 'إعدادات المرسل',
'from_settings_help' => 'حدد العنوان والاسم المستخدمين كمرسل في رسائل البريد.',
'from_address' => 'عنوان المرسل',
'from_name' => 'اسم المرسل',
'smtp' => [
'smtp_title' => 'إعدادات SMTP',
'host' => 'المضيف',
'port' => 'المنفذ',
'username' => 'اسم المستخدم',
'password' => 'كلمة المرور',
'scheme' => 'المخطط',
],
'mailgun' => [
'mailgun_title' => 'إعدادات Mailgun',
'domain' => 'النطاق',
'secret' => 'المفتاح السري',
'endpoint' => 'نقطة النهاية',
],
],
'backup' => [
'backup_driver' => 'مشغل النسخ الاحتياطي',
'throttle' => 'التقييد',
'throttle_help' => 'حدد عدد النسخ الاحتياطية التي يمكن إنشاؤها خلال فترة زمنية. قم بتعيين الفترة إلى 0 لتعطيل هذا التقييد.',
'limit' => 'الحد',
'period' => 'الفترة',
'seconds' => 'ثواني',
's3' => [
's3_title' => 'إعدادات S3',
'default_region' => 'المنطقة الافتراضية',
'access_key' => 'معرف مفتاح الوصول',
'secret_key' => 'المفتاح السري للوصول',
'bucket' => 'السعة التخزينية',
'endpoint' => 'نقطة النهاية',
'use_path_style_endpoint' => 'استخدام نقطة نهاية بأسلوب المسار',
],
],
'oauth' => [
'enable' => 'تمكين',
'enable_schema' => 'تفعيل :schema',
'disable' => 'تعطيل',
'client_id' => 'معرف العميل',
'client_secret' => 'المفتاح السري للعميل',
'redirect' => 'عنوان URL لإعادة التوجيه',
'web_api_key' => 'مفتاح API للويب',
'base_url' => 'عنوان URL الأساسي',
'display_name' => 'اسم العرض',
'auth_url' => 'عنوان URL لاستدعاء المصادقة',
'create_missing_users' => 'إنشاء تلقائي للمستخدمين المفقودين؟',
'link_missing_users' => 'ربط تلقائي للمستخدمين المفقودين؟',
],
'misc' => [
'auto_allocation' => [
'title' => 'إنشاء التخصيص التلقائي',
'helper' => 'تمكين أو تعطيل قدرة المستخدمين على إنشاء التخصيصات من خلال واجهة العميل.',
'question' => 'السماح للمستخدمين بإنشاء التخصيصات؟',
'start' => 'منفذ البداية',
'end' => 'منفذ النهاية',
],
'mail_notifications' => [
'title' => 'إشعارات البريد',
'helper' => 'تحديد الإشعارات البريدية التي يجب إرسالها إلى المستخدمين.',
'server_installed' => 'تم تثبيت الخادم',
'server_reinstalled' => 'تمت إعادة تثبيت الخادم',
],
'connections' => [
'title' => 'الاتصالات',
'helper' => 'المهلات الزمنية المستخدمة عند إجراء الطلبات.',
'request_timeout' => 'مهلة الطلب',
'connection_timeout' => 'مهلة الاتصال',
'seconds' => 'ثواني',
],
'activity_log' => [
'title' => 'سجلات الأنشطة',
'helper' => 'تحديد مدة الاحتفاظ بسجلات الأنشطة القديمة وما إذا كان يجب تسجيل أنشطة المسؤول.',
'prune_age' => 'مدة الاحتفاظ',
'days' => 'أيام',
'log_admin' => 'إخفاء أنشطة المسؤول؟',
],
'api' => [
'title' => 'واجهة API',
'helper' => 'تحديد الحد الأقصى لعدد الطلبات المسموح بها في الدقيقة.',
'client_rate' => 'حد معدل API للعميل',
'app_rate' => 'حد معدل API للتطبيق',
'rpm' => 'طلبات في الدقيقة',
],
'server' => [
'title' => 'الخوادم',
'helper' => 'إعدادات الخوادم',
'edit_server_desc' => 'السماح للمستخدمين بتعديل الأوصاف؟',
'console_font_upload' => 'رفع خط وحدة التحكم',
'console_font_hint' => 'يتم دعم خطوط *.ttf فقط. يُنصح بشدة باستخدام خطوط Mono!',
],
'webhook' => [
'title' => 'Webhook',
'helper' => 'تحديد مدة الاحتفاظ بسجلات Webhook القديمة.',
'prune_age' => 'مدة الاحتفاظ',
'days' => 'أيام',
],
],
];

18
lang/ar-SA/admin/user.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
return [
'nav_title' => 'المستخدمون',
'model_label' => 'المستخدم',
'model_label_plural' => 'المستخدمون',
'self_delete' => 'لا يمكنك حذف نفسك',
'has_servers' => 'المستخدم يمتلك خوادم',
'email' => 'البريد الإلكتروني',
'username' => 'اسم المستخدم',
'password' => 'كلمة المرور',
'password_help' => 'إدخال كلمة المرور للمستخدم اختياري. سيتلقى المستخدم الجديد بريدًا إلكترونيًا يطلب منه إنشاء كلمة مرور عند تسجيل الدخول لأول مرة.',
'admin_roles' => 'أدوار المسؤول',
'roles' => 'الأدوار',
'no_roles' => 'لا توجد أدوار',
'servers' => 'الخوادم',
'subusers' => 'المستخدمون الفرعيون',
];

View File

@@ -6,6 +6,7 @@ return [
'model_label_plural' => 'Webhooks',
'endpoint' => 'نقطة النهاية',
'description' => 'الوصف',
'events' => 'الأحداث',
'no_webhooks' => 'لا توجد Webhooks',
'help' => 'مساعدة',
'help_text' => 'يجب أن تضع اسم المتغير بين {{ }}، على سبيل المثال إذا كنت تريد الحصول على الاسم من الـ API يمكنك استخدام {{name}}.',
@@ -37,6 +38,7 @@ return [
'thumbnail' => 'عنوان الصورة المصغّرة',
'embeds' => 'ايمبدز',
'thread_name' => 'اسم موضوع المنتدى',
'flags' => 'الخيارات',
'allowed_mentions' => 'الإشارات المسموح بها',
'roles' => 'الأدوار',
'users' => 'المستخدمون',

19
lang/ar-SA/pagination.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; السابق',
'next' => 'التالي &raquo;',
];

View File

@@ -12,6 +12,4 @@ return [
'primary' => 'أساسي',
'make' => 'صنع',
'delete' => 'حذف',
'locked' => 'مقفل؟',
'locked_helper' => 'لا يمكن حذف المخصصات المقفلة إلا من قبل المشرفين',
];

View File

@@ -3,10 +3,7 @@
return [
'title' => 'بدء التشغيل',
'command' => 'أمر بدء التشغيل',
'notification_startup' => 'تم تحديث أمر البدء',
'notification_startup_body' => 'أعد تشغيل الخادم لاستخدام أمر بدء التشغيل الجديد.',
'enable_preview' => 'تمكين المعاينة',
'disable_preview' => 'تعطيل المعاينة',
'preview' => 'معاينة',
'docker_image' => 'صورة Docker',
'notification_docker' => 'تم تحديث صورة Docker',
'notification_docker_body' => 'أعد تشغيل الخادم لاستخدام الصورة الجديدة.',

View File

@@ -1,125 +0,0 @@
<?php
/**
* Contains all of the translation strings for different activity log
* events. These should be keyed by the value in front of the colon (:)
* in the event name. If there is no colon present, they should live at
* the top level.
*/
return [
'auth' => [
'fail' => 'فشل تسجيل الدخول',
'success' => 'تم تسجيل الدخول',
'password-reset' => 'إعادة تعيين كلمة المرور',
'checkpoint' => 'تم طلب المصادقة الثنائية',
'recovery-token' => 'تم استخدام رمز استعادة المصادقة الثنائية',
'token' => 'تم حل تحدي المصادقة الثنائية',
'ip-blocked' => 'تم حظر الطلب من عنوان IP غير مدرج لـ <b>:identifier</b>',
'sftp' => [
'fail' => 'فشل تسجيل الدخول عبر SFTP',
],
],
'user' => [
'account' => [
'username-changed' => 'تم تغيير اسم المستخدم من <b>:old</b> إلى <b>:new</b>',
'email-changed' => 'تم تغيير البريد الإلكتروني من <b>:old</b> إلى <b>:new</b>',
'password-changed' => 'تم تغيير كلمة المرور',
],
'api-key' => [
'create' => 'تم إنشاء مفتاح API جديد <b>:identifier</b>',
'delete' => 'تم حذف مفتاح API <b>:identifier</b>',
],
'ssh-key' => [
'create' => 'تمت إضافة مفتاح SSH <b>:fingerprint</b> إلى الحساب',
'delete' => 'تمت إزالة مفتاح SSH <b>:fingerprint</b> من الحساب',
],
'two-factor' => [
'create' => 'تم تمكين المصادقة الثنائية',
'delete' => 'تم تعطيل المصادقة الثنائية',
],
],
'server' => [
'console' => [
'command' => 'تم تنفيذ الأمر "<b>:command</b>" على الخادم',
],
'power' => [
'start' => 'تم تشغيل الخادم',
'stop' => 'تم إيقاف الخادم',
'restart' => 'تم إعادة تشغيل الخادم',
'kill' => 'تم إنهاء عملية الخادم',
],
'backup' => [
'download' => 'تم تنزيل النسخة الاحتياطية <b>:name</b>',
'delete' => 'تم حذف النسخة الاحتياطية <b>:name</b>',
'restore' => 'تمت استعادة النسخة الاحتياطية <b>:name</b> (تم حذف الملفات: <b>:truncate</b>)',
'restore-complete' => 'تمت استعادة النسخة الاحتياطية <b>:name</b> بنجاح',
'restore-failed' => 'فشلت استعادة النسخة الاحتياطية <b>:name</b>',
'start' => 'تم بدء نسخة احتياطية جديدة <b>:name</b>',
'complete' => 'تم تمييز النسخة الاحتياطية <b>:name</b> كمكتملة',
'fail' => 'تم تمييز النسخة الاحتياطية <b>:name</b> كفاشلة',
'lock' => 'تم قفل النسخة الاحتياطية <b>:name</b>',
'unlock' => 'تم فك قفل النسخة الاحتياطية <b>:name</b>',
'rename' => 'تم إعادة تسمية النسخة الاحتياطية من "<b>:old_name</b>" إلى "<b>:new_name</b>"',
],
'database' => [
'create' => 'تم إنشاء قاعدة بيانات جديدة <b>:name</b>',
'rotate-password' => 'تم تغيير كلمة مرور قاعدة البيانات <b>:name</b>',
'delete' => 'تم حذف قاعدة البيانات <b>:name</b>',
],
'file' => [
'compress' => 'تم ضغط <b>:directory:files</b>|تم ضغط <b>:count</b> ملفات في <b>:directory</b>',
'read' => 'تم عرض محتوى <b>:file</b>',
'copy' => 'تم إنشاء نسخة من <b>:file</b>',
'create-directory' => 'تم إنشاء المجلد <b>:directory:name</b>',
'decompress' => 'تم فك ضغط <b>:file</b> في <b>:directory</b>',
'delete' => 'تم حذف <b>:directory:files</b>|تم حذف <b>:count</b> ملفات في <b>:directory</b>',
'download' => 'تم تنزيل <b>:file</b>',
'pull' => 'تم تنزيل ملف عن بعد من <b>:url</b> إلى <b>:directory</b>',
'rename' => 'تم نقل/إعادة تسمية <b>:from</b> إلى <b>:to</b>|تم نقل/إعادة تسمية <b>:count</b> ملفات في <b>:directory</b>',
'write' => 'تمت كتابة محتوى جديد إلى <b>:file</b>',
'upload' => 'تم بدء رفع ملف',
'uploaded' => 'تم رفع <b>:directory:file</b>',
],
'sftp' => [
'denied' => 'تم حظر الوصول إلى SFTP بسبب الأذونات',
'create' => 'تم إنشاء <b>:files</b>|تم إنشاء <b>:count</b> ملفات جديدة',
'write' => 'تم تعديل محتوى <b>:files</b>|تم تعديل محتوى <b>:count</b> ملفات',
'delete' => 'تم حذف <b>:files</b>|تم حذف <b>:count</b> ملفات',
'create-directory' => 'تم إنشاء المجلد <b>:files</b>|تم إنشاء <b>:count</b> مجلدات',
'rename' => 'تمت إعادة تسمية <b>:from</b> إلى <b>:to</b>|تمت إعادة تسمية أو نقل <b>:count</b> ملفات',
],
'allocation' => [
'create' => 'تمت إضافة <b>:allocation</b> إلى الخادم',
'notes' => 'تم تحديث الملاحظات لـ <b>:allocation</b> من "<b>:old</b>" إلى "<b>:new</b>"',
'primary' => 'تم تعيين <b>:allocation</b> كالتخصيص الأساسي للخادم',
'delete' => 'تم حذف التخصيص <b>:allocation</b>',
],
'schedule' => [
'create' => 'تم إنشاء الجدولة <b>:name</b>',
'update' => 'تم تحديث الجدولة <b>:name</b>',
'execute' => 'تم تنفيذ الجدولة <b>:name</b> يدويًا',
'delete' => 'تم حذف الجدولة <b>:name</b>',
],
'task' => [
'create' => 'تم إنشاء مهمة جديدة "<b>:action</b>" لجدولة <b>:name</b>',
'update' => 'تم تحديث المهمة "<b>:action</b>" لجدولة <b>:name</b>',
'delete' => 'تم حذف "<b>:action</b>" لمهمة الجدول <b>:name</b>',
],
'settings' => [
'rename' => 'تمت إعادة تسمية الخادم من "<b>:old</b>" إلى "<b>:new</b>"',
'description' => 'تم تغيير وصف الخادم من "<b>:old</b>" إلى "<b>:new</b>"',
'reinstall' => 'تم إعادة تثبيت الخادم',
],
'startup' => [
'edit' => 'تم تغيير المتغير <b>:variable</b> من "<b>:old</b>" إلى "<b>:new</b>"',
'image' => 'تم تحديث صورة Docker للخادم من <b>:old</b> إلى <b>:new</b>',
'command' => 'تم تحديث أمر بدء التشغيل للخادم من <b>:old</b> إلى <b>:new</b>',
],
'subuser' => [
'create' => 'تمت إضافة <b>:email</b> كمستخدم فرعي',
'update' => 'تم تحديث أذونات المستخدم الفرعي <b>:email</b>',
'delete' => 'تمت إزالة <b>:email</b> كمستخدم فرعي',
],
'crashed' => 'تعطل الخادم',
],
];

View File

@@ -1,26 +0,0 @@
<?php
return [
'empty_table' => 'ياي! لا توجد أخطاء!',
'total_logs' => 'مجموع السجلات',
'error' => 'خطأ',
'warning' => 'تحذير',
'notice' => 'تنويه',
'info' => 'معلومات',
'debug' => 'تصحيح الأخطاء',
'navigation' => [
'panel_logs' => 'سجلات اللوحة',
],
'actions' => [
'upload_logs' => 'رفع السجلات؟',
'upload_logs_description' => 'سيؤدي هذا إلى رفع :file إلى :url هل أنت متأكد من رغبتك في القيام بهذا؟',
'view_logs' => 'عرض السجلات',
'log_not_found' => 'لم يتم العثور على السجل!',
'log_not_found_description' => 'تعذر العثور على سجل لـ :filename',
'failed_to_upload' => 'فشل الرفع.',
'failed_to_upload_description' => 'حالة HTTP: :status',
'log_upload' => 'تم رفع السجل!',
'log_upload_action' => 'عرض السجل',
'upload_tooltip' => 'رفع إلى :url',
],
];

View File

@@ -1,149 +0,0 @@
<?php
return [
'nav_title' => 'العقد',
'model_label' => 'العقدة',
'model_label_plural' => 'العقد',
'create' => 'إنشاء عقدة',
'tabs' => [
'overview' => 'نظرة عامة',
'basic_settings' => 'الإعدادات الأساسية',
'advanced_settings' => 'الإعدادات المتقدمة',
'config_file' => 'ملف الإعدادات',
'diagnostics' => 'التشخيصات',
],
'table' => [
'health' => 'الحالة',
'name' => 'الاسم',
'address' => 'العنوان',
'public' => 'عام',
'servers' => 'الخوادم',
'alias' => 'الاسم المستعار',
'ip' => 'عنوان IP',
'egg' => 'البيضة',
'owner' => 'المالك',
'allocation_notes' => 'ملاحظات',
'no_notes' => 'لا يوجد ملاحظات',
],
'node_info' => 'معلومات العقدة',
'wings_version' => 'إصدار Wings',
'cpu_threads' => 'أنوية المعالج',
'architecture' => 'المعمارية',
'kernel' => 'النواة',
'unknown' => 'غير معروف',
'latest' => 'الأحدث',
'node_uuid' => 'UUID العقدة',
'node_id' => 'معرف العقدة',
'ip_address' => 'عنوان IP',
'ip_help' => 'عادةً يكون عنوان IP العام لجهازك ما لم تكن تستخدم توجيه المنافذ.',
'alias_help' => 'اسم عرض اختياري لمساعدتك في التعرف عليها.',
'refresh' => 'تحديث',
'domain' => 'اسم النطاق',
'ssl_ip' => 'لا يمكنك الاتصال بعنوان IP عبر SSL',
'error' => 'هذا هو اسم النطاق الذي يشير إلى عنوان IP الخاص بالعقدة. إذا كنت قد قمت بإعداده مسبقًا، يمكنك التحقق منه من خلال الحقل التالي!',
'fqdn_help' => 'يتم تأمين لوحتك حاليًا عبر شهادة SSL، مما يعني أن العقد تحتاج إلى شهادة SSL أيضًا. يجب عليك استخدام اسم نطاق لأنه لا يمكن الحصول على شهادات SSL لعناوين IP.',
'dns' => 'التحقق من سجل DNS',
'dns_help' => 'يتيح لك هذا معرفة ما إذا كان سجل DNS يشير إلى عنوان IP الصحيح.',
'valid' => 'صالح',
'invalid' => 'غير صالح',
'port' => 'المنفذ',
'ports' => 'المنافذ',
'port_help' => 'إذا كنت تقوم بتشغيل Daemon خلف Cloudflare، فيجب تعيين منفذ Daemon إلى 8443 للسماح بتمرير Websocket عبر SSL.',
'connect_port' => 'منفذ الإتصال',
'connect_port_help' => 'سيتم استخدام هذا المنفذ للاتصال بـ Wings. إذا كنت تستخدم Reverse Proxy، فقد يختلف هذا عن منفذ الاستماع. عند استخدام وكيل Cloudflare، يجب استخدام المنفذ 8443.',
'listen_port' => 'منفذ الإستماع',
'listen_port_help' => 'سيستمع Wings على هذا المنفذ.',
'display_name' => 'اسم العرض',
'ssl' => 'التواصل عبر SSL',
'panel_on_ssl' => 'لوحتك تستخدم اتصال SSL آمن،<br>لذلك يجب أن يستخدم Daemon أيضًا.',
'ssl_help' => 'لا يمكن لعناوين IP استخدام SSL.',
'tags' => 'الوسوم',
'upload_limit' => 'حد الرفع',
'upload_limit_help' => 'أدخل الحد الأقصى لحجم الملفات التي يمكن رفعها عبر مدير الملفات المستند إلى الويب.',
'sftp_port' => 'منفذ SFTP',
'sftp_alias' => 'الاسم المستعار لـ SFTP',
'sftp_alias_help' => 'اسم عرض لـ SFTP، اتركه فارغًا لاستخدام FQDN الخاص بالعقدة.',
'use_for_deploy' => 'استخدام للنشر؟',
'maintenance_mode' => 'وضع الصيانة',
'maintenance_mode_help' => 'إذا تم تعيين العقدة على "تحت الصيانة"، فلن يتمكن المستخدمون من الوصول إلى الخوادم الموجودة على تلك العقدة.',
'cpu' => 'المعالج',
'cpu_limit' => 'حد المعالج',
'memory' => 'الذاكرة',
'memory_limit' => 'حد الذاكرة',
'disk' => 'القرص',
'disk_limit' => 'حد مساحة القرص',
'unlimited' => 'غير محدود',
'limited' => 'محدود',
'overallocate' => 'تجاوز الحد',
'enabled' => 'مفعل',
'disabled' => 'معطل',
'yes' => 'نعم',
'no' => 'لا',
'instructions' => 'التعليمات',
'instructions_help' => 'احفظ هذا الملف في دليل الجذر للـ Daemon، باسم config.yml',
'auto_deploy' => 'أمر النشر التلقائي',
'auto_question' => 'اختر بين التثبيت المستقل وDocker.',
'auto_label' => 'النوع',
'standalone' => 'مستقل',
'docker' => 'Docker',
'auto_command' => 'لتكوين العقدة تلقائيًا، قم بتشغيل الأمر التالي:',
'reset_token' => 'إعادة تعيين رمز المصادقة',
'token_reset' => 'تمت إعادة تعيين رمز المصادقة للـ Daemon.',
'reset_help' => 'إعادة تعيين رمز Daemon سيؤدي إلى إلغاء أي طلبات قادمة من الرمز القديم. يُستخدم هذا الرمز لجميع العمليات الحساسة على Daemon، بما في ذلك إنشاء وحذف الخوادم. نوصي بتغييره بانتظام لتعزيز الأمان.',
'no_nodes' => 'لا توجد عقد',
'none' => 'لا شيء',
'cpu_chart' => 'المعالج - :cpu% من :max%',
'memory_chart' => 'الذاكرة - :used من :total',
'disk_chart' => 'التخزين - :used من :total',
'used' => 'المستخدم',
'unused' => 'غير المستخدم',
'next_step' => 'الخطوة التالية',
'node_has_servers' => 'العقدة تحتوي على خوادم',
'create_allocation' => 'إنشاء تخصيص',
'primary_allocation' => 'التخصيص الأساسي',
'databases' => 'قواعد البيانات',
'backups' => 'النسخ الاحتياطية',
'error_connecting' => 'خطأ في الاتصال بالعقدة',
'error_connecting_description' => 'تعذر تحديث التكوين تلقائيًا على Wings، ستحتاج إلى تحديث ملف التكوين يدويًا.',
'allocation' => 'تخصيص (عنوان IP ومنفذ)',
'diagnostics' => [
'header' => 'تشخيصات العقدة',
'include_endpoints' => 'تضمين نقاط النهاية',
'include_endpoints_hint' => 'سيؤدي تضمين نقاط النهاية إلى إظهار روابط اللوحات داخل السجلات ولا يحجب عنها.',
'include_logs' => 'تضمين السجلات',
'include_logs_hint' => 'تضمين السجلات سوف يظهر السجلات الأخيرة وتساعد في تتبع المشاكل المحتملة.',
'run_diagnostics' => 'تشغيل التشخيص',
'upload_to_pelican' => 'رفع السجلات',
'logs_pulled' => 'تم سحب السجلات!',
'logs_uploaded' => 'تم رفع السجلات',
'upload_failed' => 'فشل في رفع السجلات',
'view_logs' => 'عرض السجلات',
'pull' => 'سحب',
'upload' => 'رفع',
'clear' => 'مسح',
'404' => 'تعذر العثور على تقرير التشخيص المطلوب. تأكد من أن wings محدث وحاول مرة أخرى.',
],
'cloudflare_issue' => [
'title' => 'مشكلة Cloudflare',
'body' => 'العقدة الخاصة بك لا يمكن الوصول إليها بواسطة Cloudflare',
],
'bulk_update_ip' => 'تحديث عناوين IP',
'bulk_update_ip_description' => 'استبدل عنوان IP القديم بعنوان جديد للمخصصات. هذا مفيد عندما يتغير عنوان IP للعقدة',
'update_ip' => 'تحديث عنوان IP',
'old_ip' => 'عنوان IP القديم',
'new_ip' => 'عنوان IP الجديد',
'no_allocations_to_update' => 'لم يتم العثور على مخصصات مع عنوان IP القديم المحدد',
'ip_updated' => 'تم تحديث :count بنجاح من :total المخصص(ات)',
'ip_update_failed' => 'فشل تحديث :count التخصيص(ات)',
];

View File

@@ -1,60 +0,0 @@
<?php
return [
'appsettings' => [
'comment' => [
'author' => 'قم بتوفير عنوان البريد الإلكتروني الذي يجب أن تصدر منه البيوض المصدرة بواسطة هذا اللوحة. يجب أن يكون عنوان بريد إلكتروني صالحًا.',
'url' => 'يجب أن يبدأ عنوان URL للتطبيق بـ https:// أو http:// حسب استخدامك لـ SSL أم لا. إذا لم تقم بتضمين المخطط، فقد يتم ربط رسائل البريد الإلكتروني والمحتوى الآخر بموقع غير صحيح.',
'timezone' => 'يجب أن تتطابق المنطقة الزمنية مع إحدى المناطق الزمنية المدعومة من PHP. إذا كنت غير متأكد، يرجى الرجوع إلى https://php.net/manual/en/timezones.php.',
],
'redis' => [
'note' => 'لقد اخترت برنامج Redis لسائق واحد أو أكثر، يرجى تقديم معلومات اتصال صالحة أدناه. في معظم الحالات، يمكنك استخدام الإعدادات الافتراضية ما لم تكن قد عدلت إعدادك.',
'comment' => 'بشكل افتراضي، يكون اسم المستخدم الافتراضي لخادم Redis بدون كلمة مرور لأنه يعمل محليًا وغير متاح للعالم الخارجي. إذا كان هذا هو الحال، فقط اضغط على Enter دون إدخال قيمة.',
'confirm' => 'يبدو أن هناك قيمة :field محددة بالفعل لـ Redis، هل ترغب في تغييرها؟',
],
],
'database_settings' => [
'DB_HOST_note' => 'يُوصى بشدة بعدم استخدام "localhost" كمضيف قاعدة البيانات، حيث لاحظنا مشكلات متكررة في اتصال المقبس. إذا كنت تريد استخدام اتصال محلي، فيجب أن تستخدم "127.0.0.1".',
'DB_USERNAME_note' => 'استخدام حساب الجذر لاتصالات MySQL ليس فقط مرفوضًا بشدة، ولكنه غير مسموح به في هذا التطبيق. ستحتاج إلى إنشاء مستخدم MySQL لهذا البرنامج.',
'DB_PASSWORD_note' => 'يبدو أن لديك بالفعل كلمة مرور اتصال MySQL محددة، هل ترغب في تغييرها؟',
'DB_error_2' => 'لم يتم حفظ بيانات الاعتماد الخاصة باتصالك. ستحتاج إلى تقديم معلومات اتصال صالحة قبل المتابعة.',
'go_back' => 'العودة والمحاولة مرة أخرى',
],
'make_node' => [
'name' => 'أدخل معرفًا قصيرًا لتمييز هذه العقدة عن غيرها',
'description' => 'أدخل وصفًا لتحديد العقدة',
'scheme' => 'يرجى إدخال https لاستخدام SSL أو http لاتصال غير مشفر',
'fqdn' => 'أدخل اسم النطاق (مثل node.example.com) ليتم استخدامه للاتصال بالـ Daemon. يمكن استخدام عنوان IP فقط إذا لم تكن تستخدم SSL لهذه العقدة.',
'public' => 'هل يجب أن تكون هذه العقدة عامة؟ ملاحظة: تعيين العقدة كخاصة سيمنع إمكانية النشر التلقائي لهذه العقدة.',
'behind_proxy' => 'هل اسم النطاق الخاص بك خلف وكيل؟',
'maintenance_mode' => 'هل يجب تمكين وضع الصيانة؟',
'memory' => 'أدخل الحد الأقصى للذاكرة',
'memory_overallocate' => 'أدخل مقدار الذاكرة المطلوب تجاوزه، -1 سيعطل الفحص و 0 سيمنع إنشاء خوادم جديدة',
'disk' => 'أدخل الحد الأقصى لمساحة القرص',
'disk_overallocate' => 'أدخل مقدار القرص المطلوب تجاوزه، -1 سيعطل الفحص و 0 سيمنع إنشاء خوادم جديدة',
'cpu' => 'أدخل الحد الأقصى لاستخدام المعالج',
'cpu_overallocate' => 'أدخل مقدار تجاوز استخدام المعالج، -1 سيعطل الفحص و 0 سيمنع إنشاء خوادم جديدة',
'upload_size' => 'أدخل الحد الأقصى لحجم التحميل',
'daemonListen' => 'أدخل منفذ استماع الـ Daemon',
'daemonConnect' => 'أدخل منفذ الاتصال الخاص بال daemon (يمكن أن يكون نفس منفذ الاستماع)',
'daemonSFTP' => 'أدخل منفذ استماع SFTP لـ Daemon',
'daemonSFTPAlias' => 'أدخل اسم مستعار لـ SFTP (يمكن أن يكون فارغًا)',
'daemonBase' => 'أدخل المجلد الأساسي',
'success' => 'تم إنشاء عقدة جديدة بنجاح بالاسم :name ومعرفها :id',
],
'node_config' => [
'error_not_exist' => 'العقدة المحددة غير موجودة.',
'error_invalid_format' => 'تنسيق غير صالح محدد. الخيارات الصالحة هي yaml و json.',
],
'key_generate' => [
'error_already_exist' => 'يبدو أنك قمت بالفعل بتكوين مفتاح تشفير التطبيق. المتابعة مع هذه العملية ستؤدي إلى استبدال هذا المفتاح وقد تسبب في تلف البيانات المشفرة الموجودة. لا تتابع ما لم تكن متأكدًا مما تفعله.',
'understand' => 'أفهم عواقب تنفيذ هذا الأمر وأتحمل كامل المسؤولية عن فقدان البيانات المشفرة.',
'continue' => 'هل أنت متأكد أنك تريد المتابعة؟ تغيير مفتاح تشفير التطبيق سيسبب فقدان البيانات.',
],
'schedule' => [
'process' => [
'no_tasks' => 'لا توجد مهام مجدولة للخوادم تحتاج إلى التشغيل.',
'error_message' => 'حدث خطأ أثناء معالجة الجدولة: ',
],
],
];

View File

@@ -1,18 +0,0 @@
<?php
return [
'open_server' => 'فتح الخادم',
'installation_completed' => 'اكتمل تثبيت الخادم',
'installation_failed' => 'فشل تثبيت الخادم',
'reinstallation_completed' => 'اكتملت إعادة تثبيت السير فر',
'reinstallation_failed' => 'فشل إعادة تثبيت الخادم',
'failed' => 'فشل',
'user_added' => [
'title' => 'أضيف إلى الخادم',
'body' => 'لقد تمت إضافتك كمستخدم فرعي إلى الخادم',
],
'user_removed' => [
'title' => 'تم الإزالة من الخادم',
'body' => 'لقد تمت إزالتك كمستخدم فرعي من الخادم',
],
];

View File

@@ -1,70 +0,0 @@
<?php
return [
'title' => 'الملف الشخصي',
'tabs' => [
'account' => 'الحساب',
'oauth' => 'OAuth',
'activity' => 'النشاط',
'api_keys' => 'مفاتيح API',
'ssh_keys' => 'مفاتيح SSH',
'keys' => 'المفاتيح',
'2fa' => 'المصادقة الثنائية',
'customization' => 'التخصيص',
],
'username' => 'اسم المستخدم',
'admin' => 'المدير',
'exit_admin' => 'الخروج من المسؤول',
'server_list' => 'قائمة الخوادم',
'email' => 'البريد الإلكتروني',
'password' => 'كلمة المرور',
'current_password' => 'كلمة المرور الحالية',
'password_confirmation' => 'تأكيد كلمة المرور',
'timezone' => 'المنطقة الزمنية',
'language' => 'اللغة',
'language_help' => 'لغتك :state لم تتم ترجمتها بعد!',
'link' => 'ربط',
'unlink' => 'إلغاء الربط',
'unlinked' => ':name تم إلغاء ربطه',
'scan_qr' => 'مسح رمز QR',
'code' => 'الرمز',
'setup_key' => 'مفتاح الإعداد',
'invalid_code' => 'رمز المصادقة الثنائية غير صالح',
'code_help' => 'قم بمسح رمز QR أعلاه باستخدام تطبيق المصادقة الثنائية، ثم أدخل الرمز الذي تم إنشاؤه.',
'2fa_enabled' => 'المصادقة الثنائية مفعلة حالياً!',
'backup_help' => 'لن يتم عرض هذه الأكواد مرة أخرى!',
'backup_codes' => 'أكواد النسخ الاحتياطي',
'disable_2fa' => 'تعطيل المصادقة الثنائية',
'disable_2fa_help' => 'أدخل رمز المصادقة الثنائية الحالي لتعطيل المصادقة الثنائية',
'api_keys' => 'مفاتيح API',
'create_api_key' => 'إنشاء مفتاح API',
'api_key_created' => 'تم إنشاء مفتاح API',
'description' => 'الوصف',
'allowed_ips' => 'عناوين IP المسموح بها',
'allowed_ips_help' => 'اضغط على Enter لإضافة عنوان IP جديد أو اتركه فارغًا للسماح بأي عنوان IP',
'ssh_keys' => 'مفاتيح SSH',
'create_ssh_key' => 'إنشاء مفتاح SSH',
'ssh_key_created' => 'تم إنشاء مفتاح SSH',
'name' => 'الاسم',
'public_key' => 'المفتاح العام',
'could_not_create_ssh_key' => 'تعذر إنشاء مفتاح ssh',
'dashboard' => 'لوحة التحكم',
'dashboard_layout' => 'تصميم لوحة التحكم',
'console' => 'وحدة التحكم',
'grid' => 'شبكة',
'table' => 'جدول',
'rows' => 'صفوف',
'font_size' => 'حجم الخط',
'font' => 'نوع الخط',
'font_preview' => 'معاينة الخط',
'seconds' => 'ثواني',
'graph_period' => 'فترة الرسم البياني',
'graph_period_helper' => 'كمية نقاط البيانات و الثواني المعروضة على الرسوم البيانية',
'navigation' => 'نوع التنقل',
'sidebar' => 'الشريط الجانبي',
'topbar' => 'الشريط العلوي',
'mixed' => 'مختلط',
'no_oauth' => 'لا يوجد حسابات مرتبطة',
'no_api_keys' => 'لا يوجد مفاتيح API',
'no_ssh_keys' => 'لا يوجد مفاتيح SSH',
];

View File

@@ -1,11 +0,0 @@
<?php
return [
'title' => 'النشاط',
'event' => 'الحدث',
'user' => 'المستخدم',
'deleted_user' => 'مستخدم محذوف',
'system' => 'النظام',
'timestamp' => 'التوقيت الزمني',
'metadata' => 'بيانات التعريف',
];

View File

@@ -1,43 +0,0 @@
<?php
return [
'title' => 'وحدة التحكم',
'command' => 'اكتب أمر...',
'command_blocked' => 'الخادم غير متصل...',
'command_blocked_title' => 'لا يمكن إرسال أمر عندما يكون الخادم غير متصل',
'open_in_admin' => 'فتح في لوحة المسؤول',
'power_actions' => [
'start' => 'تشغيل',
'stop' => 'إيقاف',
'restart' => 'إعادة تشغيل',
'kill' => 'قتل',
'kill_tooltip' => 'يمكن أن يؤدي هذا إلى فساد البيانات و/أو فقدان البيانات!',
],
'labels' => [
'cpu' => 'المعالج',
'memory' => 'الذاكرة',
'network' => 'الشبكة',
'disk' => 'القرص',
'name' => 'الاسم',
'status' => 'الحالة',
'address' => 'العنوان',
'unavailable' => 'غير متاح',
],
'status' => [
'created' => 'تم الإنشاء',
'starting' => 'جار التشغيل',
'running' => 'قيد التشغيل',
'restarting' => 'يتم إعادة التشغيل',
'exited' => 'تم الخروج',
'paused' => 'متوقف مؤقتاً',
'dead' => 'ميت',
'removing' => 'جار الإزالة',
'stopping' => 'جار الإيقاف',
'offline' => 'غير مُتصل',
'missing' => 'مفقود',
],
'websocket_error' => [
'title' => 'تعذر الاتصال ب websocket!',
'body' => 'تحقق من وحدة التحكم في المتصفح الخاص بك للحصول على مزيد من التفاصيل.',
],
];

View File

@@ -1,28 +0,0 @@
<?php
return [
'title' => 'الخوادم',
'list' => 'قائمة الخوادم',
'tabs' => [
'my' => 'خوادمي',
'other' => 'خوادم الآخرين',
'all' => 'جميع الخوادم',
],
'empty_own' => 'لا تملك أي خوادم!',
'empty_other' => 'ليس لديك حق الوصول إلى أي خادم!',
'status' => 'الحالة',
'server' => 'الخادم',
'resources' => 'الموارد',
'usage_limit' => 'حد الاستخدام: :resource',
'cpu' => 'المعالج',
'memory' => 'الذاكرة',
'disk' => 'القرص',
'network' => 'الشبكة',
'none' => 'لا شيء',
'loading' => 'جار التحميل...',
'power_actions' => 'إجراءات التشغيل',
'power_action_sent' => ':action أرسلت إلى :name',
];

View File

@@ -1,26 +0,0 @@
<?php
return [
'title' => 'قواعد البيانات',
'empty' => 'لا يوجد قواعد بيانات',
'create_database' => 'إنشاء قاعدة بيانات',
'limit' => 'تم الوصول إلى حد قاعدة البيانات',
'viewing' => 'عرض: :databas',
'host' => 'المضيف',
'database' => 'قاعدة البيانات',
'username' => 'اسم المستخدم',
'password' => 'كلمة المرور',
'remote' => 'بعيد',
'created_at' => 'تاريخ الانشاء',
'name' => 'اسم قاعدة البيانات',
'name_hint' => 'ترك هذا الحقل فارغًا سيؤدي إلى إنشاء اسم عشوائي تلقائيًا',
'connections_from' => 'الاتصالات من',
'max_connections' => 'الحد الأقصى للاتصالات',
'database_host' => 'مضيف قاعدة بيانات',
'database_host_select' => 'اختر مضيف قاعدة البيانات',
'jdbc' => 'سلسلة اتصال JDBC',
'create_notification' => 'تم إنشاء :database',
'create_notification_fail' => 'فشل إنشاء :database',
'delete_notification' => 'تم حذف :database',
'delete_notification_fail' => 'فشل في حذف :database',
];

View File

@@ -1,73 +0,0 @@
<?php
return [
'title' => 'المستخدمون',
'username' => 'اسم المستخدم',
'email' => 'البريد الإلكتروني',
'assign_all' => 'تعيين الكل',
'invite_user' => 'دعوة مستخدم',
'action' => 'دعوة',
'remove' => 'إزالة مستخدم',
'edit' => 'تعديل المستخدم',
'editing' => 'تعديل :user',
'delete' => 'حذف المستخدم',
'notification_add' => 'تم دعوة المستخدم!',
'notification_edit' => 'تم تحديث المستخدم!',
'notification_delete' => 'تم حذف المستخدم!',
'notification_failed' => 'فشل دعوة المستخدم!',
'permissions' => [
'title' => 'الأذونات',
'activity_desc' => 'الأذونات التي تتحكم في وصول المستخدم إلى سجلات أنشطة الخادم.',
'startup_desc' => 'الأذونات التي تتحكم في قدرة المستخدم على عرض معلمات تشغيل هذا الخادم.',
'settings_desc' => 'الأذونات التي تتحكم في قدرة المستخدم على تعديل إعدادات هذا الخادم.',
'control_desc' => 'الأذونات التي تتحكم في قدرة المستخدم على التحكم في حالة تشغيل الخادم أو إرسال الأوامر.',
'user_desc' => 'الأذونات التي تسمح للمستخدم بإدارة المستخدمين الفرعيين الآخرين على الخادم. لن يتمكنوا أبدًا من تعديل حسابهم الخاص أو منح أذونات لا يمتلكونها بأنفسهم.',
'file_desc' => 'الأذونات التي تتحكم في قدرة المستخدم على تعديل نظام الملفات لهذا الخادم.',
'allocation_desc' => 'الأذونات التي تتحكم في قدرة المستخدم على تعديل تخصيصات المنافذ لهذا الخادم.',
'database_desc' => 'الأذونات التي تتحكم في وصول المستخدم إلى إدارة قواعد البيانات لهذا الخادم.',
'backup_desc' => 'الأذونات التي تتحكم في قدرة المستخدم على إنشاء وإدارة النسخ الاحتياطية للخادم.',
'schedule_desc' => 'الأذونات التي تتحكم في وصول المستخدم إلى إدارة الجدولة لهذا الخادم.',
'startup_read' => 'يسمح للمستخدم بعرض متغيرات بدء التشغيل للخادم.',
'startup_update' => 'يسمح للمستخدم بتعديل متغيرات بدء التشغيل للخادم.',
'startup_docker_image' => 'يسمح للمستخدم بتعديل صورة Docker المستخدمة عند تشغيل الخادم.',
'settings_reinstall' => 'يسمح للمستخدم بتشغيل إعادة تثبيت هذا الخادم.',
'settings_rename' => 'يسمح للمستخدم بإعادة تسمية هذا الخادم.',
'settings_description' => 'يسمح للمستخدم بتغيير وصف هذا الخادم.',
'activity_read' => 'يسمح للمستخدم بعرض سجلات أنشطة الخادم.',
'websocket_connect' => 'يسمح للمستخدم بالوصول إلى websocket لهذا الخادم.',
'control_console' => 'يسمح للمستخدم بإرسال بيانات إلى وحدة تحكم الخادم.',
'control_start' => 'يسمح للمستخدم بتشغيل الخادم.',
'control_stop' => 'يسمح للمستخدم بإيقاف الخادم.',
'control_restart' => 'يسمح للمستخدم بإعادة تشغيل الخادم.',
'control_kill' => 'يسمح للمستخدم بإنهاء عملية الخادم.',
'user_create' => 'يسمح للمستخدم بإنشاء حسابات مستخدمين جديدة للخادم.',
'user_read' => 'يسمح للمستخدم بعرض المستخدمين المرتبطين بهذا الخادم.',
'user_update' => 'يسمح للمستخدم بتعديل المستخدمين الآخرين المرتبطين بهذا الخادم.',
'user_delete' => 'يسمح للمستخدم بحذف المستخدمين الآخرين المرتبطين بهذا الخادم.',
'file_create' => 'يسمح للمستخدم بإنشاء ملفات ومجلدات جديدة.',
'file_read' => 'يسمح للمستخدم بعرض محتويات دليل، ولكن ليس عرض محتوى أو تنزيل الملفات.',
'file_read_content' => 'يسمح للمستخدم بعرض محتوى ملف معين. سيسمح ذلك أيضًا للمستخدم بتنزيل الملفات.',
'file_update' => 'يسمح للمستخدم بتحديث الملفات والمجلدات المرتبطة بالخادم.',
'file_delete' => 'يسمح للمستخدم بحذف الملفات والمجلدات.',
'file_archive' => 'يسمح للمستخدم بإنشاء أرشيفات ملفات وفك ضغط الأرشيفات الموجودة.',
'file_sftp' => 'يسمح للمستخدم بتنفيذ العمليات السابقة باستخدام عميل SFTP.',
'allocation_read' => 'يسمح للمستخدم بعرض جميع التخصيصات المعينة حاليًا لهذا الخادم. يمكن للمستخدمين الذين لديهم أي مستوى من الوصول إلى هذا الخادم دائمًا عرض التخصيص الأساسي.',
'allocation_update' => 'يسمح للمستخدم بتغيير التخصيص الأساسي للخادم وإضافة ملاحظات لكل تخصيص.',
'allocation_delete' => 'يسمح للمستخدم بحذف تخصيص من الخادم.',
'allocation_create' => 'يسمح للمستخدم بتعيين تخصيصات إضافية للخادم.',
'database_create' => 'يسمح للمستخدم بإنشاء قاعدة بيانات جديدة للخادم.',
'database_read' => 'يسمح للمستخدم بعرض قواعد بيانات الخادم.',
'database_update' => 'يسمح للمستخدم بإجراء تعديلات على قاعدة بيانات. إذا لم يكن لدى المستخدم إذن "عرض كلمة المرور"، فلن يتمكن من تعديل كلمة المرور.',
'database_delete' => 'يسمح للمستخدم بحذف قاعدة بيانات.',
'database_view_password' => 'يسمح للمستخدم بعرض كلمة مرور قاعدة البيانات في النظام.',
'schedule_create' => 'يسمح للمستخدم بإنشاء جدول جديد للخادم.',
'schedule_read' => 'يسمح للمستخدم بعرض الجداول الزمنية للخادم.',
'schedule_update' => 'يسمح للمستخدم بتعديل جدول زمني موجود للخادم.',
'schedule_delete' => 'يسمح للمستخدم بحذف جدول زمني للخادم.',
'backup_create' => 'يسمح للمستخدم بإنشاء نسخ احتياطية جديدة لهذا الخادم.',
'backup_read' => 'يسمح للمستخدم بعرض جميع النسخ الاحتياطية الموجودة لهذا الخادم.',
'backup_delete' => 'يسمح للمستخدم بإزالة النسخ الاحتياطية من النظام.',
'backup_download' => 'يسمح للمستخدم بتنزيل نسخة احتياطية للخادم. تحذير: يسمح هذا للمستخدم بالوصول إلى جميع ملفات الخادم في النسخة الاحتياطية.',
'backup_restore' => 'يسمح للمستخدم باستعادة نسخة احتياطية للخادم. تحذير: يسمح هذا للمستخدم بحذف جميع ملفات الخادم أثناء العملية.',
],
];

View File

@@ -18,7 +18,6 @@ return [
'intro-update-available' => [
'heading' => 'Даступна абнаўленне.',
'content' => ':latestVersion цяпер даступная! Прачытайце нашу дакументацыю, каб абнавіць вашу панэль.',
'button_changelog' => 'Што новага?',
],
'intro-no-update' => [
'heading' => 'Ваша панэль абноўлена да актуальнай версіі.',

View File

@@ -0,0 +1,15 @@
<?php
return [
'model_label' => 'Планаванне',
'model_label_plural' => 'Планаванне',
'import' => [
'file' => 'Файл',
'url' => 'URL-адрас',
'schedule_help' => 'Гэта павінен быць зыходны .json-файл (schedule-daily-restart.json)',
'url_help' => 'URL-адрасы павінны весці непасрэдна да зыходнага .json-файла',
'add_url' => 'Новы URL-адрас',
'import_failed' => 'Імпартаваць не ўдалося',
'import_success' => 'Імпарт удаўся',
],
];

18
lang/be-BY/admin/user.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
return [
'nav_title' => 'Карыстальнікі',
'model_label' => 'Карыстальнік',
'model_label_plural' => 'Карыстальнікі',
'self_delete' => 'Нельга выдаліць сябе',
'has_servers' => 'Карыстальнік мае серверы',
'email' => 'Пошта',
'username' => 'Имя пользователя',
'password' => 'Пароль',
'password_help' => 'Прапанаванне пароля карыстальніка з\'яўляецца неабавязковым. Электронная пошта новага карыстальніка падказвае стварыць пароль пры першым уводзе.',
'admin_roles' => 'Ролі адміністратара',
'roles' => 'Ролі',
'no_roles' => 'Няма роляў',
'servers' => 'Серверы',
'subusers' => 'Падкарыстальнікі',
];

19
lang/be-BY/pagination.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Папярэдняя',
'next' => 'Наступная &raquo;',
];

View File

@@ -0,0 +1,15 @@
<?php
return [
'title' => 'Сетка',
'add' => 'Дадаць сетку',
'limit' => 'Дасягнуты максімальны ліміт сеткі',
'address' => 'Адрас',
'port' => 'Порт',
'notes' => 'Нататкі',
'no_notes' => 'Няма нататак',
'make_primary' => 'Сделать основным',
'primary' => 'Основной',
'make' => 'Зрабіць',
'delete' => 'Удалить',
];

View File

@@ -0,0 +1,14 @@
<?php
return [
'title' => 'Запуск',
'command' => 'Каманда запуску',
'preview' => 'Прагляд',
'docker_image' => 'Docker-вобраз',
'notification_docker' => 'Docker-вобраз абноўлены',
'notification_docker_body' => 'Перазапусціце сервер, каб выкарыстаць новы вобраз.',
'variables' => 'Зменныя сервера',
'update' => 'Абноўлено :variable',
'fail' => 'Памылка: :variable',
'validation_fail' => 'Праверка не прайшла: :variable',
];

View File

@@ -1,125 +0,0 @@
<?php
/**
* Contains all of the translation strings for different activity log
* events. These should be keyed by the value in front of the colon (:)
* in the event name. If there is no colon present, they should live at
* the top level.
*/
return [
'auth' => [
'fail' => 'Не атрымалася аўтарызавацца',
'success' => 'Увайшоў',
'password-reset' => 'Скінуць пароль',
'checkpoint' => 'Двухфактарная аўтэнтыфікацыя ўключана',
'recovery-token' => 'Использован резервный код 2FA',
'token' => 'Пройдена двухфакторная проверка',
'ip-blocked' => 'Блакаваная заявка ад неўлічанага IP-адрасу для <b>:identifier</b>',
'sftp' => [
'fail' => 'Не атрымалася аўтарызавацца',
],
],
'user' => [
'account' => [
'username-changed' => 'Зменен ідэнтыфікатар карыстальніка з <b>:old</b> на <b>:new</b>',
'email-changed' => 'Зменена электронная пошта з <b>:old</b> на <b>:new</b>',
'password-changed' => 'Змяніць пароль',
],
'api-key' => [
'create' => 'Створаны новы API ключ <b>:identifier</b>',
'delete' => 'Выдалены API ключ <b>:identifier</b>',
],
'ssh-key' => [
'create' => 'Дададзены SSH ключ <b>:fingerprint</b> да ўліковага запісу',
'delete' => 'Выдалены SSH ключ <b>:fingerprint</b> з уліковага запісу',
],
'two-factor' => [
'create' => 'Включена двухфакторная авторизация',
'delete' => 'Включена двухфакторная авторизация',
],
],
'server' => [
'console' => [
'command' => 'Выканана дзеянне <b>:command</b> на серверы',
],
'power' => [
'start' => 'Сервер запушчаны',
'stop' => 'Сервер спынены',
'restart' => 'Сервер перазапушчаны',
'kill' => 'Працэс сервера завершаны',
],
'backup' => [
'download' => 'Спампавана рэзервовая копія <b>:name</b>',
'delete' => 'Выдалена рэзервовая копія <b>:name</b>',
'restore' => 'Адноўлена рэзервовая копія <b>:name</b> (выдаленыя файлы: <b>:truncate</b>)',
'restore-complete' => 'Завершана аднаўленне рэзервовай копіі <b>:name</b>',
'restore-failed' => 'Няўдалася завяршыць аднаўленне рэзервовай копіі <b>:name</b>',
'start' => 'Пачата новая рэзервовая копія <b>:identifier</b>',
'complete' => 'Рэзервовая копія <b>:name</b> адзначана як завершаная',
'fail' => 'Рэзервовая копія <b>:name</b> адзначана як няўдалая',
'lock' => 'Замкнута рэзервовая копія <b>:name</b>',
'unlock' => 'Адкрылі рэзервовую копію <b>:name</b>',
'rename' => 'Перайменаваны рэзервовы файл з "<b>:old_name</b>" у "<b>:new_name</b>"',
],
'database' => [
'create' => 'Створана новая база дадзеных <b>:name</b>',
'rotate-password' => 'Пароль для базы даных <b>:name</b> зменены',
'delete' => 'Выдалена база дадзеных <b>:name</b>',
],
'file' => [
'compress' => 'Кампрэсаваны <b>:directory:files</b>|Кампрэсавана <b>:count</b> файлаў у <b>:directory</b>',
'read' => 'Паглядзелі змесціва файла <b>:file</b>',
'copy' => 'Створана копія файла <b>:file</b>',
'create-directory' => 'Створана тэчка <b>:directory:name</b>',
'decompress' => 'Распакоўка файла <b>:file</b> у <b>:directory</b>',
'delete' => 'Выдалены <b>:directory:files</b>|Выдалены <b>:count</b> файлаў у <b>:directory</b>',
'download' => 'Спампаваны файл <b>:file</b>',
'pull' => 'Спампаваны файл з аддаленага сэрвера з <b>:url</b> у <b>:directory</b>',
'rename' => 'Перамешчаны/ Пераназваны <b>:from</b> у <b>:to</b>|Перамешчаны/ Пераназваны <b>:count</b> файлаў у <b>:directory</b>',
'write' => 'Запісаны новы кантэнт у файл <b>:file</b>',
'upload' => 'Пачата загрузка файла',
'uploaded' => 'Загружаны файл <b>:directory:file</b>',
],
'sftp' => [
'denied' => 'Блакаваная магчымасць доступу SFTP з-за правоў',
'create' => 'Створаны <b>:files</b>|Створана <b>:count</b> новых файлаў',
'write' => 'Заменен змест у <b>:files</b>|Зменены змест <b>:count</b> файлаў',
'delete' => 'Выдалены <b>:files</b>|Выдалены <b>:count</b> файлы',
'create-directory' => 'Створана папка <b>:files</b>|Створана <b>:count</b> папак',
'rename' => 'Пераназваны <b>:from</b> у <b>:to</b>|Пераназваны або перамешчаны <b>:count</b> файлы',
],
'allocation' => [
'create' => 'Дададзена <b>:allocation</b> на сервер',
'notes' => 'Абноўлены заўвагі для <b>:allocation</b> з <b>:old</b> на <b>:new</b>',
'primary' => 'Усталявана <b>:allocation</b> як асноўная сетка для сервера',
'delete' => 'Выдалена сетка <b>:allocation</b>',
],
'schedule' => [
'create' => 'Створана задача <b>:name</b>',
'update' => 'Абноўлена задача <b>:name</b>',
'execute' => 'Уручную выканана задача <b>:name</b>',
'delete' => 'Выдалена задача <b>:name</b>',
],
'task' => [
'create' => 'Створана новая дзеянне "<b>:action</b>" для задачы "<b>:name</b>"',
'update' => 'Абноўлена дзеянне "<b>:action</b>" для задачы "<b>:name</b>".',
'delete' => 'Выдалена дзеянне "<b>:action</b>" для задачы "<b>:name</b>"',
],
'settings' => [
'rename' => 'Пераназваны сервер з "<b>:old</b>" на "<b>:new</b>"',
'description' => 'Змянёна апісанне сервера з "<b>:old</b>" на "<b>:new</b>"',
'reinstall' => 'Сервер пераўсталяваны',
],
'startup' => [
'edit' => 'Змянёна зменная "<b>:variable</b>" з "<b>:old</b>" на "<b>:new</b>"',
'image' => 'Абноўлены Docker-вобраз для сервера з "<b>:old</b>" на "<b>:new</b>"',
'command' => 'Абноўлена каманда запуску для сервера з "<b>:old</b>" на "<b>:new</b>"',
],
'subuser' => [
'create' => 'Дададзены "<b>:email</b>" як падкарыстальнік',
'update' => 'Абноўлены правы падкарыстальніка для "<b>:email</b>"',
'delete' => 'Выдалены "<b>:email</b>" як падкарыстальнік',
],
'crashed' => 'Сервер выйшаў з ладу',
],
];

View File

@@ -1,27 +0,0 @@
<?php
return [
'title' => 'API ключы прыкладання',
'empty' => 'Няма API ключоў',
'whitelist' => 'Белы спіс IPv4 адрасоў',
'whitelist_help' => 'API ключы могуць быць абмежаваны для працы з пэўнымі IPv4 адрасамі. Увядзіце кожны адрас на новым радку.',
'whitelist_placeholder' => 'Напрыклад: 127.0.0.1 або 192.168.1.1',
'description' => 'Апісанне',
'description_help' => 'Кароткае апісанне гэтага ключа.',
'nav_title' => 'API ключы',
'model_label' => 'API ключ прыкладання',
'model_label_plural' => 'API ключы прыкладання',
'table' => [
'key' => 'Ключ',
'description' => 'Апісанне',
'last_used' => 'Апошняе выкарыстанне',
'created' => 'Створаны',
'created_by' => 'Стварыў',
'never_used' => 'Не выкарыстоўвалася',
],
'permissions' => [
'none' => 'Няма',
'read' => 'Чытаць',
'read_write' => 'Чытаць і пісаць',
],
];

View File

@@ -1,24 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Гэтыя ўліковыя даныя не супадаюць з запісамі.',
'failed-two-factor' => 'Няправільны код 2FA.',
'two-factor-code' => 'Код двафактарнай аўтэнтыфікацыі',
'two-factor-hint' => 'Вы можаце выкарыстоўваць рэзервовыя коды, калі страцілі доступ да вашага прылады.',
'password' => 'Дадзены пароль няправільны.',
'throttle' => 'Занадта шмат спробаў уваходу. Калі ласка, паспрабуйце зноў праз :seconds секунд.',
'2fa_must_be_enabled' => 'Адміністратар запатрабаваў уключыць двухфактарную аўтэнтыфікацыю для вашага ўліковага запісу, каб мець магчымасць карыстацца панэллю.',
];

View File

@@ -1,60 +0,0 @@
<?php
return [
'appsettings' => [
'comment' => [
'author' => 'Увядзіце адрас электроннай пошты, з якога павінны быць адпраўлены вобразамі, экспартаваныя з гэтай панэлі. Гэта павінен быць сапраўдны адрас электроннай пошты.',
'url' => 'URL прыкладання павінен пачынацца з https:// або http:// ў залежнасці ад таго, ці выкарыстоўваецца SSL. Калі схема не будзе ўключаная, вашыя электронныя лісты і іншы кантэнт будуць спасылацца на няправільнае месца.',
'timezone' => 'Часавы пояс павінен супадаць з адным з падтрымліваемых часавых паясоў PHP. Калі вы не ўпэўнены, калі ласка, звярніцеся да https://php.net/manual/en/timezones.php.',
],
'redis' => [
'note' => 'Вы выбралі драйвер Redis для адной або некалькіх опцый, калі ласка, прадастаўце сапраўдныя дадзеныя для падключэння ніжэй. У большасці выпадкаў вы можаце выкарыстоўваць па змоўчанні параметры, калі не змянялі вашу наладу.',
'comment' => 'Па змоўчанні экземпляр Redis мае імя карыстальніка "default" і не мае пароля, паколькі ён працуе лакальна і недаступны знешняму свету. Калі гэта так, проста націсніце enter без уводу значэння.',
'confirm' => 'Выглядае, што :field ужо вызначаны для Redis, хочаце змяніць?',
],
],
'database_settings' => [
'DB_HOST_note' => 'Рекомендуецца не выкарыстоўваць "localhost" як хост базы дадзеных, бо мы часта сутыкаліся з праблемамі падключэння праз сокеты. Калі вы хочаце выкарыстоўваць лакальнае падключэнне, вам трэба выкарыстоўваць "127.0.0.1".',
'DB_USERNAME_note' => 'Выкарыстанне ўліковага запісу root для падключэнняў да MySQL не толькі настойліва не рэкамендуецца, але таксама не дазваляецца гэтым прыкладаннем. Вам трэба стварыць карыстальніка MySQL для гэтага праграмнага забеспячэння.',
'DB_PASSWORD_note' => 'Выглядае, што ў вас ужо вызначаны пароль для падключэння MySQL, хочаце яго змяніць?',
'DB_error_2' => 'Вашы дадзеныя для падключэння не былі захаваныя. Вам трэба ўвесці сапраўдныя дадзеныя для падключэння, перш чым працягнуць.',
'go_back' => 'Вярнуцца і паспрабаваць зноў',
],
'make_node' => [
'name' => 'Увядзіце кароткі ідэнтыфікатар, які выкарыстоўваецца для адрознення гэтага вузла ад іншых',
'description' => 'Увядзіце апісанне для ідэнтыфікацыі вузла',
'scheme' => 'Калі ласка, увядзіце https для SSL або http для не-ssl злучэння',
'fqdn' => 'Увядзіце даменнае імя (напрыклад, node.example.com), якое будзе выкарыстоўвацца для падключэння да дэману. IP-адрас можа выкарыстоўвацца толькі ў тым выпадку, калі вы не выкарыстоўваеце SSL для гэтага вузла',
'public' => 'Ці павінен гэты вузел быць публічным? Як заўвага, усталёўка вузла ў рэжым прыватнасці будзе адмаўляць магчымасць аўтаматычнага разгортвання на гэтым вузле',
'behind_proxy' => 'Ваш FQDN знаходзіцца за проксі?',
'maintenance_mode' => 'Ці павінен быць уключаны рэжым абслугоўвання?',
'memory' => 'Увядзіце максімальную колькасць памяці',
'memory_overallocate' => 'Увядзіце колькасць памяці для пераразмеркавання, -1 адключыць праверку, а 0 не дазволіць ствараць новыя серверы',
'disk' => 'Увядзіце максімальную колькасць месца на дыску',
'disk_overallocate' => 'Увядзіце колькасць месца на дыску для пераразмеркавання, -1 адключыць праверку, а 0 не дазволіць ствараць новыя серверы',
'cpu' => 'Увядзіце максімальную колькасць працэсарных рэсурсаў',
'cpu_overallocate' => 'Увядзіце колькасць працэсарных рэсурсаў для пераразмеркавання, -1 адключыць праверку, а 0 не дазволіць ствараць новыя серверы',
'upload_size' => 'Увядзіце максімальны памер файла для загрузкі',
'daemonListen' => 'Увядзіце порт для праслухоўвання дэману',
'daemonConnect' => 'Увядзіце порт для падключэння дэману (можа супадаць з портам слухання)',
'daemonSFTP' => 'Увядзіце порт праслухоўвання SFTP дэману',
'daemonSFTPAlias' => 'Увядзіце псеўданім SFTP дэману (можа быць пустым)',
'daemonBase' => 'Увядзіце асноўную тэчку',
'success' => 'Паспяхова створаны новы вузел з імем :name і Id :id',
],
'node_config' => [
'error_not_exist' => 'Выбраны вузел не існуе.',
'error_invalid_format' => 'Указаны няправільны фармат. Дапушчальныя фарматы: yaml і json.',
],
'key_generate' => [
'error_already_exist' => 'Выглядае, што вы ўжо наладзілі ключ шыфравання прыкладання. Працягваючы гэты працэс, вы перазапішаце гэты ключ і выклікаеце пашкоджанне дадзеных для ўжо зашыфраваных дадзеных. НЕ ПРАДАЛЖАЙЦЕ, КАЛІ НЕ ВЕДАЕЦЕ, ШТО РАБІЦЕ.',
'understand' => 'Я разумею наступствы выканання гэтай каманды і бяру на сябе ўсю адказнасць за страту зашыфраваных дадзеных.',
'continue' => 'Вы ўпэўнены, што хочаце працягнуць? Змена ключа шыфравання прыкладання прівядзе да страту дадзеных.',
],
'schedule' => [
'process' => [
'no_tasks' => 'Няма запланаваных задач для сервера, якія трэба выканаць.',
'error_message' => 'Узнікла памылка пры апрацоўцы задачы: ',
],
],
];

View File

@@ -1,64 +0,0 @@
<?php
return [
'daemon_connection_failed' => 'Узнікла памылка пры спробе ўзаемадзеяння з дэманам, што прывяло да кода адказу HTTP/:code. Гэтая памылка была запісана ў журнал.',
'node' => [
'servers_attached' => 'Вузел павінен мець адключаныя ўсе серверы, каб яго можна было выдаліць.',
'error_connecting' => 'Немагчыма падключыцца да :node',
'daemon_off_config_updated' => 'Канфігурацыя дэману <strong>была абноўлена</strong>, аднак узнікла памылка пры спробе аўтаматычна абнавіць канфігурацыйны файл дэману. Вам трэба будзе ўручную абнавіць канфігурацыйны файл (config.yml), каб дэман ужыў гэтыя змены.',
],
'allocations' => [
'server_using' => 'Сервер ужо прывязаны да гэтай сеткі. Сетка можа быць выдалена толькі ў выпадку, калі сервер не прывязаны.',
'too_many_ports' => 'Дадаць больш за 1000 портаў у адзін дыяпазон адразу не падтрымліваецца.',
'invalid_mapping' => 'Картаванне для порта :port няправільнае і не можа быць апрацавана.',
'cidr_out_of_range' => 'Натацыя CIDR дазваляе выкарыстоўваць маскі толькі ў межах /25 і /32.',
'port_out_of_range' => 'Порты ў сетцы павінны быць большымі за 1024 і меншымі або роўнымі 65535.',
],
'egg' => [
'delete_has_servers' => 'Вобраз з актыўнымі серверамі, падключанымі да яго, не можа быць выдалены з панэлі.',
'invalid_copy_id' => 'Вобраз, выбраны для капіравання скрыпта, не існуе або капіруе сам скрыпт.',
'has_children' => 'Гэты вобраз з\'яўляецца бацькам аднаго ці некалькіх іншых вобразаў. Калі ласка, выдаліце гэтыя вобразаў перад выдаленнем гэтага.',
],
'variables' => [
'env_not_unique' => 'Зменная асяроддзя :name павінна быць унікальнай для гэтага вобраза.',
'reserved_name' => 'Зменная асяроддзя :name абароненая і не можа быць прызначана іншай зменнай.',
'bad_validation_rule' => 'Правіла праверкі ":rule" не з\'яўляецца сапраўдным правілам для гэтага прыкладання.',
],
'importer' => [
'json_error' => 'Узнікла памылка пры спробе прачытаць JSON файл: :error.',
'file_error' => 'Пададзены JSON файл не з\'яўляецца сапраўдным.',
'invalid_json_provided' => 'Пададзены JSON файл не ў фармаце, які можа быць распазнаны.',
],
'subusers' => [
'editing_self' => 'Змяненне ўліковага запісу свайго падкарыстальніка не дазваляецца.',
'user_is_owner' => 'Вы не можаце дадаць ўладальніка сервера як падкарыстальніка для гэтага сервера.',
'subuser_exists' => 'Карыстальнік з гэтай поштай ужо прызначаны падкарыстальнікам для гэтага сервера.',
],
'databases' => [
'delete_has_databases' => 'Нельга выдаліць сервер базы даных, калі на ім ёсць актыўныя базы дадзеных.',
],
'tasks' => [
'chain_interval_too_long' => 'Максімальны інтэрвал для спасцяжнай задачы складае 15 хвілін.',
],
'locations' => [
'has_nodes' => 'Нельга выдаліць размяшчэнне, калі на яго прывязаны актыўныя вузлы.',
],
'users' => [
'is_self' => 'Нельга выдаліць уліковы запіс карыстальніка.',
'has_servers' => 'Нельга выдаліць карыстальніка, калі ў яго ёсць актыўныя серверы. Спачатку выдаліце іх серверы.',
'node_revocation_failed' => 'Не ўдалося адклікаць ключы на <a href=":link">вузле #:node</a>. :error',
],
'deployment' => [
'no_viable_nodes' => 'Няма вузлоў, якія адпавядаюць патрабаванням для аўтаматычнага разгортвання.',
'no_viable_allocations' => 'Няма сетак, якія адпавядаюць патрабаванням для аўтаматычнага разгортвання.',
],
'api' => [
'resource_not_found' => 'Запытаны рэсурс не існуе на гэтым серверы.',
],
'mount' => [
'servers_attached' => 'Вобраз павінен мець адключаныя ўсе серверы, каб яго можна было выдаліць.',
],
'server' => [
'marked_as_failed' => 'Гэты сервер яшчэ не завершыў працэс усталёўкі, паспрабуйце пазней.',
],
];

View File

@@ -1,70 +0,0 @@
<?php
return [
'title' => 'Профіль',
'tabs' => [
'account' => 'Уліковы запіс',
'oauth' => 'OAuth',
'activity' => 'Актыўнасць',
'api_keys' => 'API ключы',
'ssh_keys' => 'Ключы SSH',
'keys' => 'Ключы',
'2fa' => '2FA',
'customization' => 'Кастамізацыя',
],
'username' => 'Имя пользователя',
'admin' => 'Адміністратар',
'exit_admin' => 'Выйсці з адміністратара',
'server_list' => 'Спіс сервероў',
'email' => 'Пошта',
'password' => 'Пароль',
'current_password' => 'Бягучы пароль',
'password_confirmation' => 'Подтвердіть пароль',
'timezone' => 'Часавы пояс',
'language' => 'Мовы',
'language_help' => 'Ваша мова :state яшчэ не перакладзена!',
'link' => 'Спалучыць',
'unlink' => 'Адключыць',
'unlinked' => ':name адключаны',
'scan_qr' => 'Адсканаваць QR-код',
'code' => 'Код',
'setup_key' => 'Ключ налад',
'invalid_code' => 'Няправільны код 2FA',
'code_help' => 'Адсканаваць QR код, выкарыстоўваючы вашую праграму двухкрокавай аўтэнтыфікацыі, а затым увядзіце згенераваны код.',
'2fa_enabled' => 'Двухфактарная аўтэнтыфікацыя зараз уключана!',
'backup_help' => 'Яны больш не будуць паказаны!',
'backup_codes' => 'Рэзервовыя коды',
'disable_2fa' => 'Адключыць 2FA',
'disable_2fa_help' => 'Увядзіце ваш тэкучы код 2FA для адключэння двухфактарнай аўтэнтыфікацыі.',
'api_keys' => 'API ключы',
'create_api_key' => 'Стварыць API ключ',
'api_key_created' => 'Ключ API створаны',
'description' => 'Апісанне',
'allowed_ips' => 'Дазволеныя IP-адраса',
'allowed_ips_help' => 'Націсніце Enter, каб дадаць новы IP-адрас, або пакіньце пустым, каб дазволіць любы IP-адрас',
'ssh_keys' => 'Ключы SSH',
'create_ssh_key' => 'Стварыць SSH ключ',
'ssh_key_created' => 'Ключ SHH створаны',
'name' => 'Назва',
'public_key' => 'Публічны ключ',
'could_not_create_ssh_key' => 'Няўдалося стварыць SSH ключ',
'dashboard' => 'Панэль кіравання',
'dashboard_layout' => 'Макет панэлі кіравання',
'console' => 'Тэрмінал',
'grid' => 'Сетка',
'table' => 'Табліца',
'rows' => 'Радкі',
'font_size' => 'Памер шрыфта',
'font' => 'Шрыфт',
'font_preview' => 'Папярэдні прагляд шрыфту',
'seconds' => 'Секунды',
'graph_period' => 'Перыяд графіка',
'graph_period_helper' => 'Колькасць кропак даных (у секундах) на графіках у тэрмінале.',
'navigation' => 'Тып панэлі',
'sidebar' => 'Бакавая панэль',
'topbar' => 'Верхняя панэль',
'mixed' => 'Змешаны',
'no_oauth' => 'Вы пакуль не звязалі ніводнага ўліковага запісу',
'no_api_keys' => 'Няма API ключоў',
'no_ssh_keys' => 'Няма SSH ключоў',
];

View File

@@ -1,55 +0,0 @@
<?php
return [
'title' => 'Налады',
'server_info' => [
'title' => 'Інфармацыя сервера',
'information' => 'Інфармацыя',
'name' => 'Імя сервера',
'server_name' => 'Імя сервера: :name',
'notification_name' => 'Імя сервера абноўлено',
'description' => 'Апісанне сервера',
'notification_description' => 'Апісанне сервера абноўлено',
'failed' => 'Не атрымалася',
'uuid' => 'UUID сервера',
'uuid_short' => 'ID сервера',
'node_name' => 'Назва вузла',
'icon' => [
'upload' => 'Загрузіць іконку',
'tooltip' => 'Выкарыстоўваць іконку вобраза',
'updated' => 'Іконка сервера абноўлена',
'deleted' => 'Іконка сервера выдалена',
],
'limits' => [
'title' => 'Ліміты',
'unlimited' => 'Неабмежавана',
'of' => 'з :max',
'cpu' => 'Працэсар',
'memory' => 'Памяць',
'disk' => 'Месца на дыску',
'backups' => 'Рэзервовыя копіі',
'databases' => 'База даных',
'allocations' => 'Сеткі',
'no_allocations' => 'Дадатковыя сеткі адсутнічаюць',
],
'sftp' => [
'title' => 'Інфармацыя пра SFTP',
'connection' => 'Падключэнне',
'action' => 'Падключыцца да SFTP',
'username' => 'Ідэнтыфікатар карыстальніка',
'password' => 'Пароль',
'password_body' => 'Ваш пароль для SFTP супадае з паролем для ўваходу ў панэль.',
],
],
'reinstall' => [
'title' => 'Пераўсталяваць сервер',
'body' => 'Пераўсталёўка сервера спыніць яго і паўторна запусціць інсталяцыйны скрыпт, які першапачаткова яго наладзіў.',
'body2' => 'Падчас працэсу некаторыя файлы могуць быць выдалены або зменены, калі ласка, зрабіце рэзервовую копію дадзеных перад працягам.',
'action' => 'Пераўсталяваць',
'modal' => 'Ці ўпэўнены вы, што хочаце пераўсталяваць гэты сервер?',
'modal_description' => 'Падчас працэсу некаторыя файлы могуць быць выдалены або зменены, калі ласка, зрабіце рэзервовую копію дадзеных перад працягам.',
'yes' => 'Так, пераўсталяваць',
'notification_start' => 'Пераўстаноўка пачата',
'notification_fail' => 'Памылка пераўсталёўкі',
],
];

View File

@@ -0,0 +1,44 @@
<?php
return [
'heading' => 'Добре дошъл в Pelican!',
'version' => 'Верия: :version',
'advanced' => 'За напреднали',
'server' => 'Сървър',
'user' => 'Потребител',
'sections' => [
'intro-developers' => [
'heading' => 'Информация за разработчици',
'content' => 'Благодарим ви че изпробвате версията за разработка!',
'extra_note' => 'Ако намерите някакви проблеми, моля докладвайте ги в GitHub.',
'button_issues' => 'Създай проблем',
'button_features' => 'Обсъждане на характеристиките',
],
'intro-update-available' => [
'heading' => 'Налична актуализация',
'content' => ':latestVersion вече е налична! Прочети нашата докоментация за да актуализираш твоя панел.',
],
'intro-no-update' => [
'heading' => 'Вашият панел е актуален',
'content' => 'Използвате Pelican версия :version. Вашият панел е актуален!',
],
'intro-first-node' => [
'heading' => 'Не са намерени node-ове',
'content' => 'Изглежда че нямаш конфигурирани node-ове но не се притеснявай, защото можеш да натиснеш бутона за действие да създадеш първия си node.',
'extra_note' => 'Ако намерите някакви проблеми моля, докладвайте ги в GitHub.',
'button_label' => 'Създай първия си Node в Pelican',
],
'intro-support' => [
'heading' => 'Подкрепете Pelican',
'content' => 'Благодарим ви че използваш Pelican, Това можеше да се случи само чрез твойта подкрепа, на нашите сътрудници и останалите ни поддръжници!',
'extra_note' => 'Оценяваме каквато и да е подкрепа от всеки.',
'button_translate' => 'Помогнете с превода',
'button_donate' => 'Дари директно',
],
'intro-help' => [
'heading' => 'Нужда от помощ?',
'content' => 'Погледни документацията първо! Ако още ти е нужна помощ, отиди в нашият Discord сървър!',
'button_docs' => 'Прочети документацията',
],
],
];

94
lang/bg-BG/admin/egg.php Normal file
View File

@@ -0,0 +1,94 @@
<?php
return [
'nav_title' => 'Яйца',
'model_label' => 'Яйце',
'model_label_plural' => 'Яйца',
'tabs' => [
'configuration' => 'Конфигурация',
'process_management' => 'Управление на процесите',
'egg_variables' => 'Стойности на яйцето',
'install_script' => 'Инсталирай Script',
],
'import' => [
'file' => 'Файл',
'url' => 'URL адрес',
'egg_help' => 'Това трябва да е .json/.yaml файл',
'url_help' => 'URL адресите трябва да сочат директно към .json/.yaml файлът',
'add_url' => 'Нов URL адрес',
'import_failed' => 'Неуспешно импортиране',
'import_success' => 'Успешно импортиране',
'github' => 'Добави от GitHub',
'refresh' => 'Обнови',
],
'export' => [
'modal' => 'Как бихте искали да експортирате :egg ?',
'as' => 'Като .:format',
],
'in_use' => 'В употреба',
'servers' => 'Сървъри',
'name' => 'Име',
'egg_uuid' => 'UUID на яйцето',
'egg_id' => 'ID на яйцето',
'name_help' => 'Просто име, което да се използва като идентификатор за това яйце.',
'author' => 'Автор',
'uuid_help' => 'Това е глобален уникален идентикатор за това яйце, който Wings използва като идентификатор.',
'author_help' => 'Авторът на версията на това яйце.',
'author_help_edit' => 'Авторът на тази версия на яйцето. Kaчването на нова конфигурация за това яйце от друг автор ще промени това.',
'description' => 'Описание',
'description_help' => 'Описание на яйцето, което ще се показва в панела, когато е необходимо.',
'startup' => 'Startup команда',
'startup_help' => 'Startup командата, която ще бъде използвана за нови сървъри, използващи това яйце.',
'file_denylist' => 'Списък с файлове за отказ',
'file_denylist_help' => 'Списък с файлове, които крайният потребител няма право да редактира.',
'features' => 'Функции',
'force_ip' => 'Принуди изходящо IP',
'force_ip_help' => 'Принуждава целия изходящ трафик да има своето изходно IP NAT-нато до IP адреса на основния IP адрес на сървъра. Това е необходимо за правилната работа на определени игри, когато Node-а разполага с няколко публични IP адреса. Активирането на тази опция ще деактивира вътрешната мрежа за всички сървъри, използващи това яйце, което ще им попречи да имат вътрешен достъп до други сървъри на същия node.',
'tags' => 'Тагове',
'update_url' => 'Обнови URL адрес',
'update_url_help' => 'URL адресите трябва да сочат директно към .json/.yaml файлът',
'add_image' => 'Добави Docker образ',
'docker_images' => 'Docker Image-ове',
'docker_name' => 'Име на образа',
'docker_uri' => 'URL адрес на образа',
'docker_help' => 'Наличните docker образи достъпни до сървъри, които използват това яйце.',
'stop_command' => 'Stop команда',
'stop_command_help' => 'Командата, която трябва да се изпрати на сървърните процеси, за да ги спре плавно. Ако е необходимо да изпратите SIGINT, трябва да въведете ^C тук.',
'copy_from' => 'Копирай настройки от',
'copy_from_help' => 'Ако искате да използвате настройки по подразбиране от друго яйце, изберете го от менюто по-горе.',
'none' => 'Нито един',
'start_config' => 'Стартова конфигурация',
'start_config_help' => 'Списък със стойности, които daemon-ът трябва да търси при стартирането на сървъра, за да определи дали то е завършено.',
'config_files' => 'Конфигурационни файлове',
'config_files_help' => 'Това трябва да бъде JSON представяне на конфигурационни файлове, които трябва да бъдат модифицирани, и частите, които трябва да бъдат променени.',
'log_config' => 'Запиши конфигурацията във файл',
'log_config_help' => 'Това трябва да е JSON представяне на мястото, където се съхраняват лог файловете, и дали daemon-ът трябва да създава персонализирани лог файлове.',
'environment_variable' => 'Променливи на средата',
'default_value' => 'Стойност по подразбиране',
'user_permissions' => 'Права на потребител',
'viewable' => 'Достъпен за преглед',
'editable' => 'Редактируем',
'rules' => 'Правила',
'add_new_variable' => 'Добави нова променлива',
'error_unique' => 'Променлива с това име вече съществува.',
'error_required' => 'Полето за променлива на средата е задължително.',
'error_reserved' => 'Тази променлива на средата е резервирана и не може да бъде използвана.',
'script_from' => 'Script от',
'script_container' => 'Script контейнер',
'script_entry' => 'Входна точка на Script-а',
'script_install' => 'Инсталирай Script',
'no_eggs' => 'Няма яйца',
'no_servers' => 'Няма сървъри',
'no_servers_help' => 'Няма сървъри, които да използват това яйце',
'update' => 'Обнови|Избрано обновяване',
'updated' => 'Яйцето е обновено|:count/:total яйца обновени',
'updated_failed' => ':count неуспешно',
'update_question' => 'Сигурни ли сте, че искате да обновите това яйце?|Наистина ли искате да обновите избраните яйца?',
'update_description' => 'Ако сте направили някакви промени в яйцето, те ще бъдат презаписани!|Ако сте направили някакви промени в яйцето, те ще бъдат презаписани!',
'no_updates' => 'Няма налични обновления за избраните яйца',
];

19
lang/bg-BG/pagination.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Предишно',
'next' => 'Следващо &raquo;',
];

View File

@@ -21,7 +21,6 @@ return [
],
'user' => [
'account' => [
'username-changed' => 'Změněno uživatelské jméno z <b>:old</b> na <b>:new</b>',
'email-changed' => 'Změněný e-mail z <b>:old</b> na <b>:new</b>',
'password-changed' => 'Změněné heslo',
],
@@ -113,7 +112,6 @@ return [
'startup' => [
'edit' => 'Změnil proměnnou <b>:variable</b> z "<b>:old</b>" na "<b>:new</b>"',
'image' => 'Aktualizoval Docker Image pro server z <b>:old</b> na <b>:new</b>',
'command' => 'Aktualizován příkaz pro spuštění pro server z <b>:old</b> na <b>:new</b>',
],
'subuser' => [
'create' => 'Přidáno <b>:email</b> jako poduživatel',

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