Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
487dc5a1cb ci(release): bump version 2025-07-06 21:05:55 +00:00
567 changed files with 3627 additions and 23185 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,4 +56,9 @@ abstract class BaseSchema
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
return true;
}
}

View File

@@ -26,5 +26,10 @@ interface CaptchaSchemaInterface
public function getIcon(): ?string;
public function validateResponse(?string $captchaResponse = null): void;
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array;
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool;
}

View File

@@ -4,7 +4,6 @@ namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\CaptchaService;
use Closure;
use Exception;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\App;
@@ -12,12 +11,10 @@ class Rule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
App::call(fn (CaptchaService $service) => $service->get('turnstile')->validateResponse($value));
} catch (Exception $exception) {
report($exception);
$response = App::call(fn (CaptchaService $service) => $service->getActiveSchema()->validateResponse($value));
$fail('Captcha validation failed: ' . $exception->getMessage());
if (!$response['success']) {
$fail($response['message'] ?? 'Unknown error occurred, please try again');
}
}
}

View File

@@ -66,9 +66,9 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
}
/**
* @throws Exception
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): void
public function validateResponse(?string $captchaResponse = null): array
{
$captchaResponse ??= request()->get('cf-turnstile-response');
@@ -83,33 +83,22 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret,
'response' => $captchaResponse,
])
->json();
]);
if (!$response['success']) {
match ($response['error-codes'][0] ?? null) {
'missing-input-secret' => throw new Exception('The secret parameter was not passed.'),
'invalid-input-secret' => throw new Exception('The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.'),
'missing-input-response' => throw new Exception('The response parameter (token) was not passed.'),
'invalid-input-response' => throw new Exception('The response parameter (token) is invalid or has expired.'),
'bad-request' => throw new Exception('The request was rejected because it was malformed.'),
'timeout-or-duplicate' => throw new Exception('The response parameter (token) has already been validated before.'),
default => throw new Exception('An internal error happened while validating the response.'),
};
}
if (!$this->verifyDomain($response['hostname'] ?? '')) {
throw new Exception('Domain verification failed.');
}
return count($response->json()) ? $response->json() : [
'success' => false,
'message' => 'Unknown error occurred, please try again',
];
}
private function verifyDomain(string $hostname): bool
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true;
}
$requestUrl = parse_url(request()->url());
$requestUrl ??= request()->url;
$requestUrl = parse_url($requestUrl);
return $hostname === array_get($requestUrl, 'host');
}

View File

@@ -32,8 +32,4 @@ interface OAuthSchemaInterface
public function getHexColor(): ?string;
public function isEnabled(): bool;
public function shouldCreateMissingUsers(): bool;
public function shouldLinkMissingUsers(): bool;
}

View File

@@ -5,9 +5,7 @@ namespace App\Extensions\OAuth\Schemas;
use App\Extensions\OAuth\OAuthSchemaInterface;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Set;
use Illuminate\Support\Str;
abstract class OAuthSchema implements OAuthSchemaInterface
@@ -55,28 +53,6 @@ abstract class OAuthSchema implements OAuthSchemaInterface
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_SECRET")),
Toggle::make("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")
->label(trans('admin/setting.oauth.create_missing_users'))
->columnSpanFull()
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")),
Toggle::make("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")
->label(trans('admin/setting.oauth.link_missing_users'))
->columnSpanFull()
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")),
];
}
@@ -120,18 +96,4 @@ abstract class OAuthSchema implements OAuthSchemaInterface
return env("OAUTH_{$id}_ENABLED", false);
}
public function shouldCreateMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
}
}

View File

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

View File

@@ -119,7 +119,7 @@ class Settings extends Page implements HasForms
->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('oauth')
Tab::make('OAuth')
->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth')
->schema($this->oauthSettings()),
@@ -169,6 +169,16 @@ class Settings extends Page implements HasForms
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
->default(env('APP_DEBUG', config('app.debug'))),
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
->label(trans('admin/setting.general.navigation'))
->inline()
->options([
false => trans('admin/setting.general.sidebar'),
true => trans('admin/setting.general.topbar'),
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
]),
Group::make()
->columns(2)
@@ -553,7 +563,7 @@ class Settings extends Page implements HasForms
->label(trans('admin/setting.oauth.enable'))
->color('success')
->steps($schema->getSetupSteps())
->modalHeading(trans('admin/setting.oauth.enable_schema', ['schema' => $schema->getName()]))
->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $schema->getName())
->modalSubmitActionLabel(trans('admin/setting.oauth.enable'))
->modalCancelAction(false)
->action(function ($data, Set $set) use ($key) {

View File

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

View File

@@ -29,7 +29,7 @@ class EggResource extends Resource
public static function getNavigationGroup(): ?string
{
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
}
public static function getNavigationLabel(): string

View File

@@ -57,8 +57,7 @@ class CreateEgg extends CreateRecord
return $form
->schema([
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
Tab::make(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([
TextInput::make('name')
@@ -124,8 +123,7 @@ class CreateEgg extends CreateRecord
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
Tab::make(trans('admin/egg.tabs.process_management'))
->columns()
->schema([
CopyFrom::make('copy_process_from')
@@ -148,8 +146,7 @@ class CreateEgg extends CreateRecord
->default('{}')
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
Tab::make(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->schema([
Repeater::make('variables')
@@ -210,7 +207,7 @@ class CreateEgg extends CreateRecord
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
@@ -242,8 +239,7 @@ class CreateEgg extends CreateRecord
]),
]),
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
Tab::make(trans('admin/egg.tabs.install_script'))
->columns(3)
->schema([
CopyFrom::make('copy_script_from')
@@ -258,11 +254,7 @@ class CreateEgg extends CreateRecord
->native(false)
->selectablePlaceholder(false)
->default('bash')
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->options(['bash', 'ash', '/bin/bash'])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))

View File

@@ -44,8 +44,7 @@ class EditEgg extends EditRecord
return $form
->schema([
Tabs::make()->tabs([
Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
Tab::make(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->icon('tabler-egg')
->schema([
@@ -116,8 +115,7 @@ class EditEgg extends EditRecord
->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')),
]),
Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
Tab::make(trans('admin/egg.tabs.process_management'))
->columns()
->icon('tabler-server-cog')
->schema([
@@ -137,8 +135,7 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')),
]),
Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
Tab::make(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull()
->icon('tabler-variable')
->schema([
@@ -199,7 +196,7 @@ class EditEgg extends EditRecord
'*' => trans('admin/egg.error_reserved'),
])
->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255),
Fieldset::make(trans('admin/egg.user_permissions'))
->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
@@ -231,8 +228,7 @@ class EditEgg extends EditRecord
]),
]),
]),
Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
Tab::make(trans('admin/egg.tabs.install_script'))
->columns(3)
->icon('tabler-file-download')
->schema([
@@ -247,11 +243,7 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.script_entry'))
->native(false)
->selectablePlaceholder(false)
->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->options(['bash', 'ash', '/bin/bash'])
->required(),
MonacoEditor::make('script_install')
->label(trans('admin/egg.script_install'))

View File

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

View File

@@ -40,8 +40,7 @@ class NodeResource extends Resource
public static function getNavigationGroup(): ?string
{
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
}
public static function getNavigationBadge(): ?string

View File

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

View File

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

View File

@@ -62,16 +62,7 @@ class AllocationsRelationManager extends RelationManager
->label(trans('admin/node.table.allocation_notes'))
->placeholder(trans('admin/node.table.no_notes')),
SelectColumn::make('ip')
->options(function (Allocation $allocation) {
$ips = Allocation::where('port', $allocation->port)->pluck('ip');
return collect($this->getOwnerRecord()->ipAddresses())
->diff($ips)
->unshift($allocation->ip)
->unique()
->mapWithKeys(fn (string $ip) => [$ip => $ip])
->all();
})
->options(fn (Allocation $allocation) => collect($this->getOwnerRecord()->ipAddresses())->merge([$allocation->ip])->mapWithKeys(fn (string $ip) => [$ip => $ip]))
->selectablePlaceholder(false)
->searchable()
->label(trans('admin/node.table.ip')),

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ class RoleResource extends Resource
public static function getNavigationGroup(): ?string
{
return !empty(auth()->user()->getCustomization()['top_navigation']) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
return config('panel.filament.top-navigation', false) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
}
public static function getNavigationBadge(): ?string
@@ -129,6 +129,7 @@ class RoleResource extends Resource
->required()
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
TextInput::make('guard_name')
->label('Guard Name')
->default(Role::DEFAULT_GUARD_NAME)
->nullable()
->hidden(),

View File

@@ -43,7 +43,7 @@ class ServerResource extends Resource
public static function getNavigationGroup(): ?string
{
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
}
public static function getNavigationBadge(): ?string

View File

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

View File

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

View File

@@ -56,7 +56,7 @@ class UserResource extends Resource
public static function getNavigationGroup(): ?string
{
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.user');
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.user');
}
public static function getNavigationBadge(): ?string
@@ -80,7 +80,7 @@ class UserResource extends Resource
->label(trans('admin/user.email'))
->icon('tabler-mail'),
IconColumn::make('use_totp')
->label(trans('profile.tabs.2fa'))
->label('2FA')
->visibleFrom('lg')
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
->boolean(),

View File

@@ -19,7 +19,6 @@ use Filament\Forms\Components\Section;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration;
use Filament\Forms\Get;
@@ -130,12 +129,23 @@ class WebhookResource extends Resource
->live()
->inline()
->options(WebhookType::class)
->default(WebhookType::Regular->value),
->default(WebhookType::Regular->value)
->afterStateHydrated(function (string $state) {
if ($state === WebhookType::Discord->value) {
self::sendHelpBanner();
}
})
->afterStateUpdated(function (string $state) {
if ($state === WebhookType::Discord->value) {
self::sendHelpBanner();
}
}),
TextInput::make('description')
->label(trans('admin/webhook.description'))
->required(),
TextInput::make('endpoint')
->label(trans('admin/webhook.endpoint'))
->activeUrl()
->required()
->columnSpanFull()
->afterStateUpdated(fn (string $state, Set $set) => $set('type', str($state)->contains('discord.com') ? WebhookType::Discord->value : WebhookType::Regular->value)),
@@ -143,15 +153,6 @@ class WebhookResource extends Resource
->hidden(fn (Get $get) => $get('type') === WebhookType::Discord->value)
->dehydratedWhenHidden()
->schema(fn () => self::getRegularFields())
->headerActions([
Action::make('reset_headers')
->label(trans('admin/webhook.reset_headers'))
->color('danger')
->icon('heroicon-o-trash')
->action(fn (Get $get, Set $set) => $set('headers', [
'X-Webhook-Event' => '{{event}}',
])),
])
->formBefore(),
Section::make(trans('admin/webhook.discord'))
->hidden(fn (Get $get) => $get('type') === WebhookType::Regular->value)
@@ -162,6 +163,8 @@ class WebhookResource extends Resource
->aside()
->formBefore(),
Section::make(trans('admin/webhook.events'))
->collapsible()
->collapsed(fn (Get $get) => count($get('events') ?? []))
->schema([
CheckboxList::make('events')
->live()
@@ -180,10 +183,7 @@ class WebhookResource extends Resource
{
return [
KeyValue::make('headers')
->label(trans('admin/webhook.headers'))
->default(fn () => [
'X-Webhook-Event' => '{{event}}',
]),
->label(trans('admin/webhook.headers')),
];
}
@@ -211,7 +211,7 @@ class WebhookResource extends Resource
TextInput::make('thread_name')
->label(trans('admin/webhook.discord_message.forum_thread')),
CheckboxList::make('flags')
->label(trans('admin/webhook.discord_embed.flags'))
->label('Flags')
->options([
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds'),
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications'),

View File

@@ -63,15 +63,4 @@ class CreateWebhookConfiguration extends CreateRecord
return $data;
}
protected function getRedirectUrl(): string
{
return EditWebhookConfiguration::getUrl(['record' => $this->getRecord()]);
}
public function mount(): void
{
parent::mount();
WebhookResource::sendHelpBanner();
}
}

View File

@@ -123,10 +123,4 @@ class EditWebhookConfiguration extends EditRecord
{
$this->dispatch('refresh-widget');
}
public function mount(int|string $record): void
{
parent::mount($record);
WebhookResource::sendHelpBanner();
}
}

View File

@@ -61,13 +61,13 @@ class ListServers extends ListRecords
{
return [
TextColumn::make('condition')
->label(trans('server/dashboard.status'))
->label('Status')
->badge()
->tooltip(fn (Server $server) => $server->formatResource(ServerResourceType::Uptime))
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
->icon(fn (Server $server) => $server->condition->getIcon())
->color(fn (Server $server) => $server->condition->getColor()),
TextColumn::make('name')
->label(trans('server/dashboard.title'))
->label('Server')
->description(fn (Server $server) => $server->description)
->grow()
->searchable(),
@@ -78,22 +78,22 @@ class ListServers extends ListRecords
->copyable(request()->isSecure())
->state(fn (Server $server) => $server->allocation->address ?? 'None'),
TextColumn::make('cpuUsage')
->label(trans('server/dashboard.resources'))
->label('Resources')
->icon('tabler-cpu')
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::CPULimit)]))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::CPU))
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('cpu', limit: true, type: ServerResourceType::Percentage, precision: 0))
->state(fn (Server $server) => $server->formatResource('cpu_absolute', type: ServerResourceType::Percentage))
->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
TextColumn::make('memoryUsage')
->label('')
->icon('tabler-device-desktop-analytics')
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::MemoryLimit)]))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::Memory))
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true))
->state(fn (Server $server) => $server->formatResource('memory_bytes'))
->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
TextColumn::make('diskUsage')
->label('')
->icon('tabler-device-sd-card')
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::DiskLimit)]))
->state(fn (Server $server) => $server->formatResource(ServerResourceType::Disk))
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true))
->state(fn (Server $server) => $server->formatResource('disk_bytes'))
->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
];
}
@@ -110,7 +110,7 @@ class ListServers extends ListRecords
->poll('15s')
->columns($usingGrid ? $this->gridColumns() : $this->tableColumns())
->recordUrl(!$usingGrid ? (fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server)) : null)
->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions(view: 'table')) : [])
->actions(!$usingGrid ? ActionGroup::make(static::getPowerActions()) : [])
->actionsAlignment(Alignment::Center->value)
->contentGrid($usingGrid ? ['default' => 1, 'md' => 2] : null)
->emptyStateIcon('tabler-brand-docker')
@@ -142,18 +142,15 @@ class ListServers extends ListRecords
$other = (clone $all)->whereNot('owner_id', auth()->user()->id);
return [
'my' => Tab::make('my')
->label(trans('server/dashboard.tabs.my'))
'my' => Tab::make('My Servers')
->badge(fn () => $my->count())
->modifyQueryUsing(fn () => $my),
'other' => Tab::make('other')
->label(trans('server/dashboard.tabs.other'))
'other' => Tab::make('Others\' Servers')
->badge(fn () => $other->count())
->modifyQueryUsing(fn () => $other),
'all' => Tab::make('all')
->label(trans('server/dashboard.tabs.all'))
'all' => Tab::make('All Servers')
->badge($all->count()),
];
}
@@ -207,8 +204,8 @@ class ListServers extends ListRecords
$this->daemonPowerRepository->setServer($server)->send($action);
Notification::make()
->title(trans('server/dashboard.power_actions'))
->body(trans('server/dashboard.power_action_sent', ['action' => $action, 'name' => $server->name]))
->title('Power Action')
->body($action . ' sent to ' . $server->name)
->success()
->send();
@@ -224,46 +221,40 @@ class ListServers extends ListRecords
}
/** @return Action[]|ActionGroup[] */
public static function getPowerActions(string $view): array
public static function getPowerActions(): array
{
$actions = [
Action::make('start')
->color('primary')
->icon('tabler-player-play-filled')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']),
Action::make('restart')
->color('gray')
->icon('tabler-reload')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isRestartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']),
Action::make('stop')
->color('danger')
->icon('tabler-player-stop-filled')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStoppable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']),
Action::make('kill')
->color('danger')
->icon('tabler-alert-square')
->tooltip('This can result in data corruption and/or data loss!')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isKillable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']),
];
if ($view === 'table') {
return $actions;
} else {
return [
ActionGroup::make($actions)
->icon('tabler-power')
return [
ActionGroup::make([
Action::make('start')
->color('primary')
->tooltip('Power Actions')
->iconSize(IconSize::Large),
];
}
->icon('tabler-player-play-filled')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_START, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'start']),
Action::make('restart')
->color('gray')
->icon('tabler-reload')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_RESTART, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isRestartable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'restart']),
Action::make('stop')
->color('danger')
->icon('tabler-player-stop-filled')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isStoppable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'stop']),
Action::make('kill')
->color('danger')
->icon('tabler-alert-square')
->tooltip('This can result in data corruption and/or data loss!')
->authorize(fn (Server $server) => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
->visible(fn (Server $server) => !$server->isInConflictState() & $server->retrieveStatus()->isKillable())
->dispatch('powerAction', fn (Server $server) => ['server' => $server, 'action' => 'kill']),
])
->icon(fn (Server $server) => $server->condition->getIcon())
->color(fn (Server $server) => $server->condition->getColor())
->tooltip(fn (Server $server) => $server->condition->getLabel())
->iconSize(IconSize::Large),
];
}
}

View File

@@ -2,12 +2,9 @@
namespace App\Filament\Components\Actions;
use App\Enums\EggFormat;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use Filament\Actions\Action;
use Filament\Forms\Components\Placeholder;
use Filament\Support\Enums\Alignment;
class ExportEggAction extends Action
{
@@ -24,30 +21,8 @@ class ExportEggAction extends Action
$this->authorize(fn () => auth()->user()->can('export egg'));
$this->modalHeading(fn (Egg $egg) => trans('filament-actions::export.modal.actions.export.label') . ' ' . $egg->name);
$this->modalIcon($this->icon);
$this->form([
Placeholder::make('')
->label(fn (Egg $egg) => trans('admin/egg.export.modal', ['egg' => $egg->name])),
]);
$this->modalFooterActionsAlignment(Alignment::Center);
$this->modalFooterActions([
Action::make('json')
->label(trans('admin/egg.export.as', ['format' => 'json']))
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id, EggFormat::JSON);
}, 'egg-' . $egg->getKebabName() . '.json'))
->close(),
Action::make('yaml')
->label(trans('admin/egg.export.as', ['format' => 'yaml']))
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id, EggFormat::YAML);
}, 'egg-' . $egg->getKebabName() . '.yaml'))
->close(),
]);
$this->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json'));
}
}

View File

@@ -1,34 +0,0 @@
<?php
namespace App\Filament\Components\Actions;
use App\Models\Permission;
use App\Models\Schedule;
use App\Models\Server;
use App\Services\Schedules\Sharing\ScheduleExporterService;
use Filament\Actions\Action;
use Filament\Facades\Filament;
class ExportScheduleAction extends Action
{
public static function getDefaultName(): ?string
{
return 'export';
}
protected function setUp(): void
{
parent::setUp();
/** @var Server $server */
$server = Filament::getTenant();
$this->label(trans('filament-actions::export.modal.actions.export.label'));
$this->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_READ, $server));
$this->action(fn (ScheduleExporterService $service, Schedule $schedule) => response()->streamDownload(function () use ($service, $schedule) {
echo $service->handle($schedule);
}, 'schedule-' . str($schedule->name)->kebab()->lower()->trim() . '.json'));
}
}

View File

@@ -47,31 +47,13 @@ class ImportEggAction extends Action
foreach ($eggs as $egg) {
if ($egg instanceof TemporaryUploadedFile) {
$originalName = $egg->getClientOriginalName();
$filename = str($originalName)->afterLast('egg-');
$ext = str($originalName)->afterLast('.')->lower()->toString();
$name = match ($ext) {
'json' => $filename->before('.json')->headline(),
'yaml' => $filename->before('.yaml')->headline(),
'yml' => $filename->before('.yml')->headline(),
default => $filename->headline(),
};
$name = str($egg->getClientOriginalName())->afterLast('egg-')->before('.json')->headline();
$method = 'fromFile';
} else {
$egg = str($egg);
$egg = $egg->contains('github.com') ? $egg->replaceFirst('blob', 'raw') : $egg;
$name = $egg->afterLast('/egg-')->before('.json')->headline();
$method = 'fromUrl';
$filename = $egg->afterLast('/egg-');
$ext = $filename->afterLast('.')->lower()->toString();
$name = match ($ext) {
'json' => $filename->before('.json')->headline(),
'yaml' => $filename->before('.yaml')->headline(),
'yml' => $filename->before('.yml')->headline(),
default => $filename->headline(),
};
}
try {
$eggImportService->$method($egg);
@@ -106,21 +88,19 @@ class ImportEggAction extends Action
Tabs::make('Tabs')
->contained(false)
->tabs([
Tab::make('file')
->label(trans('admin/egg.import.file'))
Tab::make(trans('admin/egg.import.file'))
->icon('tabler-file-upload')
->schema([
FileUpload::make('files')
->label(trans('admin/egg.model_label'))
->hint(trans('admin/egg.import.egg_help'))
->acceptedFileTypes(['application/json', 'application/yaml', 'application/x-yaml', 'text/yaml'])
->acceptedFileTypes(['application/json'])
->preserveFilenames()
->previewable(false)
->storeFiles(false)
->multiple($isMultiple),
]),
Tab::make('url')
->label(trans('admin/egg.import.url'))
Tab::make(trans('admin/egg.import.url'))
->icon('tabler-world-upload')
->schema([
Select::make('github')
@@ -145,7 +125,7 @@ class ImportEggAction extends Action
}),
Repeater::make('urls')
->label('')
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->beforeLast('.')->headline())
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->before('.json')->headline())
->hint(trans('admin/egg.import.url_help'))
->addActionLabel(trans('admin/egg.import.add_url'))
->grid($isMultiple ? 2 : null)
@@ -159,7 +139,7 @@ class ImportEggAction extends Action
->label(trans('admin/egg.import.url'))
->placeholder('https://github.com/pelican-eggs/generic/blob/main/nodejs/egg-node-js-generic.json')
->url()
->endsWith(['.json', '.yaml', '.yml'])
->endsWith('.json')
->validationAttribute(trans('admin/egg.import.url')),
]),
]),

View File

@@ -1,123 +0,0 @@
<?php
namespace App\Filament\Components\Actions;
use App\Models\Permission;
use App\Models\Server;
use App\Services\Schedules\Sharing\ScheduleImporterService;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Illuminate\Support\Arr;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class ImportScheduleAction extends Action
{
public static function getDefaultName(): ?string
{
return 'import';
}
protected function setUp(): void
{
parent::setUp();
/** @var Server $server */
$server = Filament::getTenant();
$this->label(trans('filament-actions::import.modal.actions.import.label'));
$this->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_CREATE, $server));
$this->form([
Tabs::make('Tabs')
->contained(false)
->tabs([
Tab::make('file')
->label(trans('server/schedule.import_action.file'))
->icon('tabler-file-upload')
->schema([
FileUpload::make('files')
->hiddenLabel()
->hint(trans('server/schedule.import_action.schedule_help'))
->acceptedFileTypes(['application/json'])
->preserveFilenames()
->previewable(false)
->storeFiles(false)
->multiple(true),
]),
Tab::make('url')
->label(trans('server/schedule.import_action.url'))
->icon('tabler-world-upload')
->schema([
Repeater::make('urls')
->hiddenLabel()
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/schedule-')->before('.json')->headline())
->hint(trans('server/schedule.import_action.url_help'))
->addActionLabel(trans('server/schedule.import_action.add_url'))
->grid(2)
->reorderable(false)
->addable(true)
->deletable(fn (array $state) => count($state) > 1)
->schema([
TextInput::make('url')
->live()
->label(trans('server/schedule.import_action.url'))
->url()
->endsWith('.json')
->validationAttribute(trans('server/schedule.import_action.url')),
]),
]),
]),
]);
$this->action(function (array $data, ScheduleImporterService $service) use ($server) {
$schedules = array_merge(collect($data['urls'])->flatten()->whereNotNull()->unique()->all(), Arr::wrap($data['files']));
if (empty($schedules)) {
return;
}
[$success, $failed] = [collect(), collect()];
foreach ($schedules as $schedule) {
if ($schedule instanceof TemporaryUploadedFile) {
$name = str($schedule->getClientOriginalName())->afterLast('schedule-')->before('.json')->headline();
$method = 'fromFile';
} else {
$schedule = str($schedule);
$schedule = $schedule->contains('github.com') ? $schedule->replaceFirst('blob', 'raw') : $schedule;
$name = $schedule->afterLast('/schedule-')->before('.json')->headline();
$method = 'fromUrl';
}
try {
$service->$method($schedule, $server);
$success->push($name);
} catch (Exception $exception) {
$failed->push($name);
report($exception);
}
}
if ($failed->count() > 0) {
Notification::make()
->title(trans('server/schedule.import_action.import_failed'))
->body($failed->join(', '))
->danger()
->send();
}
if ($success->count() > 0) {
Notification::make()
->title(trans('server/schedule.import_action.import_success'))
->body($success->join(', '))
->success()
->send();
}
});
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace App\Filament\Components\Forms\Actions;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Get;
use Filament\Forms\Set;
class CronPresetAction extends Action
{
protected string $minute = '0';
protected string $hour = '0';
protected string $dayOfMonth = '*';
protected string $month = '*';
protected string $dayOfWeek = '*';
protected function setUp(): void
{
parent::setUp();
$this->disabled(fn (string $operation) => $operation === 'view');
$this->color(fn (Get $get) => $get('cron_minute') == $this->minute &&
$get('cron_hour') == $this->hour &&
$get('cron_day_of_month') == $this->dayOfMonth &&
$get('cron_month') == $this->month &&
$get('cron_day_of_week') == $this->dayOfWeek
? 'success' : 'primary');
$this->action(function (Set $set) {
$set('cron_minute', $this->minute);
$set('cron_hour', $this->hour);
$set('cron_day_of_month', $this->dayOfMonth);
$set('cron_month', $this->month);
$set('cron_day_of_week', $this->dayOfWeek);
});
}
public function cron(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): static
{
$this->minute = $minute;
$this->hour = $hour;
$this->dayOfMonth = $dayOfMonth;
$this->month = $month;
$this->dayOfWeek = $dayOfWeek;
return $this;
}
}

View File

@@ -15,11 +15,6 @@ class PreviewStartupAction extends Action
return 'preview';
}
public function getLabel(): string
{
return trans('server/startup.preview');
}
protected function setUp(): void
{
parent::setUp();

View File

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

View File

@@ -2,11 +2,8 @@
namespace App\Filament\Components\Tables\Actions;
use App\Enums\EggFormat;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use Filament\Forms\Components\Placeholder;
use Filament\Support\Enums\Alignment;
use Filament\Tables\Actions\Action;
class ExportEggAction extends Action
@@ -26,30 +23,8 @@ class ExportEggAction extends Action
$this->authorize(fn () => auth()->user()->can('export egg'));
$this->modalHeading(fn (Egg $egg) => trans('filament-actions::export.modal.actions.export.label') . ' ' . $egg->name);
$this->modalIcon($this->icon);
$this->form([
Placeholder::make('')
->label(fn (Egg $egg) => trans('admin/egg.export.modal', ['egg' => $egg->name])),
]);
$this->modalFooterActionsAlignment(Alignment::Center);
$this->modalFooterActions([
Action::make('json')
->label(trans('admin/egg.export.as', ['format' => 'json']))
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id, EggFormat::JSON);
}, 'egg-' . $egg->getKebabName() . '.json'))
->close(),
Action::make('yaml')
->label(trans('admin/egg.export.as', ['format' => 'yaml']))
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id, EggFormat::YAML);
}, 'egg-' . $egg->getKebabName() . '.yaml'))
->close(),
]);
$this->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json'));
}
}

View File

@@ -86,8 +86,7 @@ class ImportEggAction extends Action
Tabs::make('Tabs')
->contained(false)
->tabs([
Tab::make('file')
->label(trans('admin/egg.import.file'))
Tab::make(trans('admin/egg.import.file'))
->icon('tabler-file-upload')
->schema([
FileUpload::make('files')
@@ -99,8 +98,7 @@ class ImportEggAction extends Action
->storeFiles(false)
->multiple($isMultiple),
]),
Tab::make('url')
->label(trans('admin/egg.import.url'))
Tab::make(trans('admin/egg.import.url'))
->icon('tabler-world-upload')
->schema([
Select::make('github')
@@ -119,7 +117,7 @@ class ImportEggAction extends Action
}
}),
Repeater::make('urls')
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->beforeLast('.')->headline())
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->before('.json')->headline())
->hint(trans('admin/egg.import.url_help'))
->addActionLabel(trans('admin/egg.import.add_url'))
->grid($isMultiple ? 2 : null)
@@ -133,7 +131,7 @@ class ImportEggAction extends Action
->label(trans('admin/egg.import.url'))
->placeholder('https://github.com/pelican-eggs/generic/blob/main/nodejs/egg-node-js-generic.json')
->url()
->endsWith(['.json', '.yaml', '.yml'])
->endsWith('.json')
->validationAttribute(trans('admin/egg.import.url')),
]),
]),

View File

@@ -86,8 +86,7 @@ class EditProfile extends BaseEditProfile
->schema([
Tabs::make()->persistTabInQueryString()
->schema([
Tab::make('account')
->label(trans('profile.tabs.account'))
Tab::make(trans('profile.tabs.account'))
->icon('tabler-user')
->schema([
TextInput::make('username')
@@ -140,8 +139,7 @@ class EditProfile extends BaseEditProfile
->live()
->default('en')
->selectablePlaceholder(false)
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? ''
: trans('profile.language_help', ['state' => $state]) . ' <u><a href="https://crowdin.com/project/pelican-dev/">Update On Crowdin</a></u>'))
->helperText(fn ($state, LanguageService $languageService) => new HtmlString($languageService->isLanguageTranslated($state) ? '' : trans('profile.language_help', ['state' => $state])))
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
->native(false),
FileUpload::make('avatar')
@@ -161,8 +159,7 @@ class EditProfile extends BaseEditProfile
}),
]),
Tab::make('oauth')
->label(trans('profile.tabs.oauth'))
Tab::make(trans('profile.tabs.oauth'))
->icon('tabler-brand-oauth')
->visible(count($oauthSchemas) > 0)
->schema(function () use ($oauthSchemas) {
@@ -176,7 +173,7 @@ class EditProfile extends BaseEditProfile
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
$actions[] = Action::make("oauth_$id")
->label(trans('profile.' . ($unlink ? 'unlink' : 'link'), ['name' => $name]))
->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name)
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
->color(Color::hex($schema->getHexColor()))
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
@@ -201,8 +198,7 @@ class EditProfile extends BaseEditProfile
return [Actions::make($actions)];
}),
Tab::make('2fa')
->label(trans('profile.tabs.2fa'))
Tab::make(trans('profile.tabs.2fa'))
->icon('tabler-shield-lock')
->schema(function (TwoFactorSetupService $setupService) {
if ($this->getUser()->use_totp) {
@@ -266,7 +262,7 @@ class EditProfile extends BaseEditProfile
->content(fn () => new HtmlString("
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
"))
->helperText(trans('profile.setup_key', ['secret' => $secret])),
->helperText(trans('profile.setup_key') .': '. $secret),
TextInput::make('2facode')
->label(trans('profile.code'))
->requiredWith('2fapassword')
@@ -279,8 +275,7 @@ class EditProfile extends BaseEditProfile
];
}),
Tab::make('api_keys')
->label(trans('profile.tabs.api_keys'))
Tab::make(trans('profile.tabs.api_keys'))
->icon('tabler-key')
->schema([
Grid::make('name')->columns(5)->schema([
@@ -326,7 +321,6 @@ class EditProfile extends BaseEditProfile
Section::make(trans('profile.api_keys'))->columnSpan(2)->schema([
Repeater::make('api_keys')
->hiddenLabel()
->inlineLabel(false)
->relationship('apiKeys')
->addable(false)
->itemLabel(fn ($state) => $state['identifier'])
@@ -362,8 +356,7 @@ class EditProfile extends BaseEditProfile
]),
]),
Tab::make('ssh_keys')
->label(trans('profile.tabs.ssh_keys'))
Tab::make(trans('profile.tabs.ssh_keys'))
->icon('tabler-lock-code')
->schema([
Grid::make('name')->columns(5)->schema([
@@ -412,7 +405,6 @@ class EditProfile extends BaseEditProfile
Section::make(trans('profile.ssh_keys'))->columnSpan(2)->schema([
Repeater::make('ssh_keys')
->hiddenLabel()
->inlineLabel(false)
->relationship('sshKeys')
->addable(false)
->itemLabel(fn ($state) => $state['name'])
@@ -448,27 +440,22 @@ class EditProfile extends BaseEditProfile
]),
]),
Tab::make('activity')
->label(trans('profile.tabs.activity'))
Tab::make(trans('profile.tabs.activity'))
->icon('tabler-history')
->schema([
Repeater::make('activity')
->hiddenLabel()
->inlineLabel(false)
->label('')
->deletable(false)
->addable(false)
->relationship(null, function (Builder $query) {
$query->orderBy('timestamp', 'desc');
})
->schema([
Placeholder::make('log')
->hiddenLabel()
->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
]),
]),
Tab::make('customization')
->label(trans('profile.tabs.customization'))
Tab::make(trans('profile.tabs.customization'))
->icon('tabler-adjustments')
->schema([
Section::make(trans('profile.dashboard'))
@@ -483,14 +470,6 @@ class EditProfile extends BaseEditProfile
'grid' => trans('profile.grid'),
'table' => trans('profile.table'),
]),
ToggleButtons::make('top_navigation')
->label(trans('profile.navigation'))
->inline()
->required()
->options([
true => trans('profile.top'),
false => trans('profile.side'),
]),
]),
Section::make(trans('profile.console'))
->collapsible()
@@ -557,7 +536,7 @@ class EditProfile extends BaseEditProfile
return new HtmlString(<<<HTML
<style>
{$style}
{$style}
</style>
<span class="preview-text">The quick blue pelican jumps over the lazy pterodactyl. :)</span>
HTML);
@@ -648,10 +627,9 @@ class EditProfile extends BaseEditProfile
'console_rows' => $data['console_rows'],
'console_graph_period' => $data['console_graph_period'],
'dashboard_layout' => $data['dashboard_layout'],
'top_navigation' => $data['top_navigation'],
];
unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout'], $data['top_navigation']);
unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout']);
$data['customization'] = json_encode($moarbetterdata);
return $data;
@@ -666,7 +644,6 @@ class EditProfile extends BaseEditProfile
$data['console_rows'] = $moarbetterdata['console_rows'] ?? 30;
$data['console_graph_period'] = $moarbetterdata['console_graph_period'] ?? 30;
$data['dashboard_layout'] = $moarbetterdata['dashboard_layout'] ?? 'grid';
$data['top_navigation'] = $moarbetterdata['top_navigation'] ?? false;
return $data;
}

View File

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

View File

@@ -141,12 +141,8 @@ class Console extends Page
#[On('console-status')]
public function receivedConsoleUpdate(?string $state = null): void
{
/** @var Server $server */
$server = Filament::getTenant();
if ($state) {
$this->status = ContainerStatus::from($state);
cache()->put("servers.$server->uuid.status", $this->status, now()->addSeconds(15));
}
$this->cachedHeaderActions = [];
@@ -162,7 +158,6 @@ class Console extends Page
return [
Action::make('start')
->label(trans('server/console.power_actions.start'))
->color('primary')
->size(ActionSize::ExtraLarge)
->dispatch('setServerState', ['state' => 'start', 'uuid' => $server->uuid])
@@ -170,7 +165,6 @@ class Console extends Page
->disabled(fn () => $server->isInConflictState() || !$this->status->isStartable())
->icon('tabler-player-play-filled'),
Action::make('restart')
->label(trans('server/console.power_actions.restart'))
->color('gray')
->size(ActionSize::ExtraLarge)
->dispatch('setServerState', ['state' => 'restart', 'uuid' => $server->uuid])
@@ -178,7 +172,6 @@ class Console extends Page
->disabled(fn () => $server->isInConflictState() || !$this->status->isRestartable())
->icon('tabler-reload'),
Action::make('stop')
->label(trans('server/console.power_actions.stop'))
->color('danger')
->size(ActionSize::ExtraLarge)
->dispatch('setServerState', ['state' => 'stop', 'uuid' => $server->uuid])
@@ -187,9 +180,8 @@ class Console extends Page
->disabled(fn () => $server->isInConflictState() || !$this->status->isStoppable())
->icon('tabler-player-stop-filled'),
Action::make('kill')
->label(trans('server/console.power_actions.kill'))
->color('danger')
->tooltip(trans('server/console.power_actions.kill_tooltip'))
->tooltip('This can result in data corruption and/or data loss!')
->size(ActionSize::ExtraLarge)
->dispatch('setServerState', ['state' => 'kill', 'uuid' => $server->uuid])
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
@@ -197,14 +189,4 @@ class Console extends Page
->icon('tabler-alert-square'),
];
}
public static function getNavigationLabel(): string
{
return trans('server/console.title');
}
public function getTitle(): string
{
return trans('server/console.title');
}
}

View File

@@ -17,6 +17,7 @@ use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Support\Enums\Alignment;
use Illuminate\Support\Number;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class Settings extends ServerFormPage
@@ -38,7 +39,7 @@ class Settings extends ServerFormPage
'lg' => 6,
])
->schema([
Section::make(trans('server/setting.server_info.title'))
Section::make('Server Information')
->columns([
'default' => 1,
'sm' => 2,
@@ -46,11 +47,11 @@ class Settings extends ServerFormPage
'lg' => 6,
])
->schema([
Fieldset::make()
->label(trans('server/setting.server_info.information'))
Fieldset::make('Server')
->label('Information')
->schema([
TextInput::make('name')
->label(trans('server/setting.server_info.name'))
->label('Server Name')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
->required()
->columnSpan([
@@ -62,7 +63,7 @@ class Settings extends ServerFormPage
->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)),
Textarea::make('description')
->label(trans('server/setting.server_info.description'))
->label('Server Description')
->hidden(!config('panel.editable_server_descriptions'))
->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
->columnSpan([
@@ -75,7 +76,7 @@ class Settings extends ServerFormPage
->live(onBlur: true)
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
TextInput::make('uuid')
->label(trans('server/setting.server_info.uuid'))
->label('Server UUID')
->columnSpan([
'default' => 1,
'sm' => 1,
@@ -84,12 +85,12 @@ class Settings extends ServerFormPage
])
->disabled(),
TextInput::make('id')
->label(trans('server/setting.server_info.id'))
->label('Server ID')
->disabled()
->columnSpan(1),
]),
Fieldset::make()
->label(trans('server/setting.server_info.limits.title'))
Fieldset::make('Limits')
->label('Limits')
->columns([
'default' => 1,
'sm' => 1,
@@ -99,56 +100,57 @@ class Settings extends ServerFormPage
->schema([
TextInput::make('cpu')
->label('')
->prefix(trans('server/setting.server_info.limits.cpu'))
->prefix('CPU')
->prefixIcon('tabler-cpu')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : format_number($server->cpu) . '%'),
->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : Number::format($server->cpu, locale: auth()->user()->language) . '%'),
TextInput::make('memory')
->label('')
->prefix(trans('server/setting.server_info.limits.memory'))
->prefix('Memory')
->prefixIcon('tabler-device-desktop-analytics')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : convert_bytes_to_readable($server->memory * 2 ** 20)),
->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : convert_bytes_to_readable($server->memory * 2 ** 20)),
TextInput::make('disk')
->label('')
->prefix(trans('server/setting.server_info.limits.disk'))
->prefix('Disk Space')
->prefixIcon('tabler-device-sd-card')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : convert_bytes_to_readable($server->disk * 2 ** 20)),
->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : convert_bytes_to_readable($server->disk * 2 ** 20)),
TextInput::make('backup_limit')
->label('')
->prefix(trans('server/setting.server_info.limits.backups'))
->prefix('Backups')
->prefixIcon('tabler-file-zip')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/backup.empty') : $server->backups->count() . ' ' .trans('server/setting.server_info.limits.of', ['max' => $state])),
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups' : $server->backups->count() . ' of ' . $state),
TextInput::make('database_limit')
->label('')
->prefix(trans('server/setting.server_info.limits.databases'))
->prefix('Databases')
->prefixIcon('tabler-database')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/database.empty') : $server->databases->count() . ' ' . trans('server/setting.server_info.limits.of', ['max' => $state])),
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases' : $server->databases->count() . ' of ' . $state),
TextInput::make('allocation_limit')
->label('')
->prefix(trans('server/setting.server_info.limits.allocations'))
->prefix('Allocations')
->prefixIcon('tabler-network')
->columnSpan(1)
->disabled()
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.no_allocations') : $server->allocations->count() . ' ' .trans('server/setting.server_info.limits.of', ['max' => $state])),
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Additional Allocations' : $server->allocations->count() . ' of ' . $state),
]),
]),
Section::make(trans('server/setting.node_info.title'))
Section::make('Node Information')
->schema([
TextInput::make('node.name')
->label(trans('server/setting.node_info.name'))
->label('Node Name')
->formatStateUsing(fn (Server $server) => $server->node->name)
->disabled(),
Fieldset::make(trans('server/setting.node_info.sftp.title'))
Fieldset::make('SFTP Information')
->hidden(fn () => !auth()->user()->can(Permission::ACTION_FILE_SFTP, $server))
->label('SFTP Information')
->columns([
'default' => 1,
'sm' => 1,
@@ -157,13 +159,13 @@ class Settings extends ServerFormPage
])
->schema([
TextInput::make('connection')
->label(trans('server/setting.node_info.sftp.connection'))
->label('Connection')
->columnSpan(1)
->disabled()
->suffixAction(fn () => request()->isSecure() ? CopyAction::make() : null)
->hintAction(
Action::make('connect_sftp')
->label(trans('server/setting.node_info.sftp.action'))
->label('Connect to SFTP')
->color('success')
->icon('tabler-plug')
->url(function (Server $server) {
@@ -178,29 +180,28 @@ class Settings extends ServerFormPage
return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
}),
TextInput::make('username')
->label(trans('server/setting.node_info.sftp.username'))
->label('Username')
->columnSpan(1)
->suffixAction(fn () => request()->isSecure() ? CopyAction::make() : null)
->disabled()
->formatStateUsing(fn (Server $server) => auth()->user()->username . '.' . $server->uuid_short),
Placeholder::make('password')
->label(trans('server/setting.node_info.sftp.password'))
->columnSpan(1)
->content(trans('server/setting.node_info.sftp.password_body')),
->content('Your SFTP password is the same as the password you use to access this panel.'),
]),
]),
Section::make(trans('server/setting.reinstall.title'))
Section::make('Reinstall Server')
->hidden(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
->collapsible()
->footerActions([
Action::make('reinstall')
->label(trans('server/setting.reinstall.action'))
->color('danger')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
->label('Reinstall')
->requiresConfirmation()
->modalHeading(trans('server/setting.reinstall.modal'))
->modalDescription(trans('server/setting.reinstall.modal_description'))
->modalSubmitActionLabel(trans('server/setting.reinstall.yes'))
->modalHeading('Are you sure you want to reinstall the server?')
->modalDescription('Some files may be deleted or modified during this process, please back up your data before continuing.')
->modalSubmitActionLabel('Yes, Reinstall')
->action(function (Server $server, ReinstallServerService $reinstallService) {
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403);
@@ -210,9 +211,9 @@ class Settings extends ServerFormPage
report($exception);
Notification::make()
->title(trans('server/setting.reinstall.notification_fail'))
->body($exception->getMessage())
->danger()
->title('Server Reinstall failed')
->body($exception->getMessage())
->send();
return;
@@ -222,8 +223,8 @@ class Settings extends ServerFormPage
->log();
Notification::make()
->title(trans('server/setting.reinstall.notification_start'))
->success()
->title('Server Reinstall started')
->send();
redirect(Console::getUrl());
@@ -232,9 +233,9 @@ class Settings extends ServerFormPage
->footerActionsAlignment(Alignment::Right)
->schema([
Placeholder::make('')
->label(trans('server/setting.reinstall.body')),
->label('Reinstalling your server will stop it, and then re-run the installation script that initially set it up.'),
Placeholder::make('')
->label(trans('server/setting.reinstall.body2')),
->label('Some files may be deleted or modified during this process, please back up your data before continuing.'),
]),
]);
}
@@ -257,15 +258,15 @@ class Settings extends ServerFormPage
}
Notification::make()
->title(trans('server/setting.server_info.notification_name'))
->body(fn () => $original . ' -> ' . $name)
->success()
->title('Updated Server Name')
->body(fn () => $original . ' -> ' . $name)
->send();
} catch (Exception $exception) {
Notification::make()
->title(trans('server/setting.server_info.failed'))
->body($exception->getMessage())
->danger()
->title('Failed')
->body($exception->getMessage())
->send();
}
}
@@ -288,26 +289,16 @@ class Settings extends ServerFormPage
}
Notification::make()
->title(trans('server/setting.server_info.notification_description'))
->body(fn () => $original . ' -> ' . $description)
->success()
->title('Updated Server Description')
->body(fn () => $original . ' -> ' . $description)
->send();
} catch (Exception $exception) {
Notification::make()
->title(trans('server/setting.server_info.failed'))
->body($exception->getMessage())
->danger()
->title('Failed')
->body($exception->getMessage())
->send();
}
}
public function getTitle(): string
{
return trans('server/setting.title');
}
public static function getNavigationLabel(): string
{
return trans('server/setting.title');
}
}

View File

@@ -18,7 +18,6 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Validator;
@@ -44,7 +43,7 @@ class Startup extends ServerFormPage
Hidden::make('previewing')
->default(false),
Textarea::make('startup')
->label(trans('server/startup.command'))
->label('Startup Command')
->columnSpan([
'default' => 1,
'sm' => 1,
@@ -52,10 +51,10 @@ class Startup extends ServerFormPage
'lg' => 4,
])
->autosize()
->hintAction(PreviewStartupAction::make())
->hintAction(PreviewStartupAction::make('preview'))
->readOnly(),
TextInput::make('custom_image')
->label(trans('server/startup.docker_image'))
->label('Docker Image')
->readOnly()
->visible(fn (Server $server) => !in_array($server->image, $server->egg->docker_images))
->formatStateUsing(fn (Server $server) => $server->image)
@@ -66,7 +65,7 @@ class Startup extends ServerFormPage
'lg' => 2,
]),
Select::make('image')
->label(trans('server/startup.docker_image'))
->label('Docker Image')
->live()
->visible(fn (Server $server) => in_array($server->image, $server->egg->docker_images))
->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
@@ -81,8 +80,8 @@ class Startup extends ServerFormPage
}
Notification::make()
->title(trans('server/startup.notification_docker'))
->body(trans('server/startup.notification_docker_body'))
->title('Docker image updated')
->body('Restart the server to use the new image.')
->success()
->send();
})
@@ -98,10 +97,10 @@ class Startup extends ServerFormPage
'md' => 2,
'lg' => 2,
]),
Section::make(trans('server/startup.variables'))
Section::make('Server Variables')
->schema([
Repeater::make('server_variables')
->hiddenLabel()
->label('')
->relationship('serverVariables', fn (Builder $query) => $query->where('egg_variables.user_viewable', true)->orderByPowerJoins('variable.sort'))
->grid()
->disabled(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
@@ -208,9 +207,9 @@ class Startup extends ServerFormPage
if ($validator->fails()) {
Notification::make()
->title(trans('server/startup.validation_fail', ['variable' => $serverVariable->variable->name]))
->body(implode(', ', $validator->errors()->all()))
->danger()
->title('Validation Failed: ' . $serverVariable->variable->name)
->body(implode(', ', $validator->errors()->all()))
->send();
return null;
@@ -233,28 +232,18 @@ class Startup extends ServerFormPage
->log();
}
Notification::make()
->title(trans('server/startup.update', ['variable' => $serverVariable->variable->name]))
->body(fn () => $original . ' -> ' . $state)
->success()
->title('Updated: ' . $serverVariable->variable->name)
->body(fn () => $original . ' -> ' . $state)
->send();
} catch (\Exception $e) {
Notification::make()
->title(trans('server/startup.fail', ['variable' => $serverVariable->variable->name]))
->body($e->getMessage())
->danger()
->title('Failed: ' . $serverVariable->variable->name)
->body($e->getMessage())
->send();
}
return null;
}
public function getTitle(): string|Htmlable
{
return trans('server/startup.title');
}
public static function getNavigationLabel(): string
{
return trans('server/startup.title');
}
}

View File

@@ -38,6 +38,10 @@ class ActivityResource extends Resource
protected static ?string $model = ActivityLog::class;
protected static ?string $modelLabel = 'Activity';
protected static ?string $pluralModelLabel = 'Activity';
protected static ?int $navigationSort = 8;
protected static ?string $navigationIcon = 'tabler-stack';
@@ -52,16 +56,14 @@ class ActivityResource extends Resource
->defaultPaginationPageOption(25)
->columns([
TextColumn::make('event')
->label(trans('server/activity.event'))
->html()
->description(fn ($state) => $state)
->icon(fn (ActivityLog $activityLog) => $activityLog->getIcon())
->formatStateUsing(fn (ActivityLog $activityLog) => $activityLog->getLabel()),
TextColumn::make('user')
->label(trans('server/activity.user'))
->state(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? trans('server/activity.system') : trans('server/activity.deleted_user');
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
@@ -77,7 +79,6 @@ class ActivityResource extends Resource
->url(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor) ? EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin') : '')
->grow(false),
DateTimeColumn::make('timestamp')
->label(trans('server/activity.timestamp'))
->since()
->sortable()
->grow(false),
@@ -88,13 +89,11 @@ class ActivityResource extends Resource
//->visible(fn (ActivityLog $activityLog) => $activityLog->hasAdditionalMetadata())
->form([
Placeholder::make('event')
->label(trans('server/activity.event'))
->content(fn (ActivityLog $activityLog) => new HtmlString($activityLog->getLabel())),
TextInput::make('user')
->label(trans('server/activity.user'))
->formatStateUsing(function (ActivityLog $activityLog) use ($server) {
if (!$activityLog->actor instanceof User) {
return $activityLog->actor_id === null ? trans('server/activity.system') : trans('server/activity.deleted_user');
return $activityLog->actor_id === null ? 'System' : 'Deleted user';
}
$user = $activityLog->actor->username;
@@ -117,10 +116,9 @@ class ActivityResource extends Resource
->visible(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User && auth()->user()->can('update', $activityLog->actor))
->url(fn (ActivityLog $activityLog) => EditUser::getUrl(['record' => $activityLog->actor], panel: 'admin'))
),
DateTimePicker::make('timestamp')
->label(trans('server/activity.timestamp')),
DateTimePicker::make('timestamp'),
KeyValue::make('properties')
->label(trans('server/activity.metadata'))
->label('Metadata')
->formatStateUsing(fn ($state) => Arr::dot($state)),
]),
])
@@ -170,9 +168,4 @@ class ActivityResource extends Resource
'index' => Pages\ListActivities::route('/'),
];
}
public static function getNavigationLabel(): string
{
return trans('server/activity.title');
}
}

View File

@@ -18,9 +18,4 @@ class ListActivities extends ListRecords
{
return [];
}
public function getTitle(): string
{
return trans('server/activity.title');
}
}

View File

@@ -30,6 +30,10 @@ class AllocationResource extends Resource
protected static ?string $model = Allocation::class;
protected static ?string $modelLabel = 'Network';
protected static ?string $pluralModelLabel = 'Network';
protected static ?int $navigationSort = 7;
protected static ?string $navigationIcon = 'tabler-network';
@@ -42,17 +46,16 @@ class AllocationResource extends Resource
return $table
->columns([
TextColumn::make('ip')
->label(trans('server/network.address'))
->label('Address')
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
TextColumn::make('alias')
->hidden(),
TextColumn::make('port')
->label(trans('server/network.port')),
TextColumn::make('port'),
TextInputColumn::make('notes')
->label(trans('server/network.notes'))
->visibleFrom('sm')
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
->placeholder(trans('server/network.no_notes')),
->label('Notes')
->placeholder('No Notes'),
IconColumn::make('primary')
->icon(fn ($state) => match ($state) {
true => 'tabler-star-filled',
@@ -62,15 +65,15 @@ class AllocationResource extends Resource
true => 'warning',
default => 'gray',
})
->tooltip(fn (Allocation $allocation) => $allocation->id === $server->allocation_id ? trans('server/network.primary') : trans('server/network.make_primary'))
->tooltip(fn (Allocation $allocation) => ($allocation->id === $server->allocation_id ? 'Already' : 'Make') . ' Primary')
->action(fn (Allocation $allocation) => auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server) && $server->update(['allocation_id' => $allocation->id]))
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
->label(trans('server/network.primary')),
->label('Primary'),
])
->actions([
DetachAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
->label(trans('server/network.delete'))
->label('Delete')
->icon('tabler-trash')
->action(function (Allocation $allocation) {
Allocation::query()->where('id', $allocation->id)->update([
@@ -114,9 +117,4 @@ class AllocationResource extends Resource
'index' => Pages\ListAllocations::route('/'),
];
}
public static function getNavigationLabel(): string
{
return trans('server/network.title');
}
}

View File

@@ -13,7 +13,6 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Facades\Filament;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListAllocations extends ListRecords
{
@@ -30,10 +29,8 @@ class ListAllocations extends ListRecords
return [
Action::make('addAllocation')
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'tabler-network-off' : 'tabler-network')
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
->tooltip(fn () => $server->allocations()->count() >= $server->allocation_limit ? trans('server/network.limit') : trans('server/network.add'))
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
->hidden(fn () => !config('panel.client_features.allocations.enabled'))
->disabled(fn () => $server->allocations()->count() >= $server->allocation_limit)
->color(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'danger' : 'primary')
@@ -56,14 +53,4 @@ class ListAllocations extends ListRecords
{
return [];
}
public function getTitle(): string
{
return trans('server/network.title');
}
public static function getNavigationLabel(): string
{
return trans('server/network.title');
}
}

View File

@@ -79,15 +79,14 @@ class BackupResource extends Resource
return $form
->schema([
TextInput::make('name')
->label(trans('server/backup.actions.create.name'))
->label('Name')
->columnSpanFull(),
TextArea::make('ignored')
->label(trans('server/backup.actions.create.ignored'))
->columnSpanFull(),
->columnSpanFull()
->label('Ignored Files & Directories'),
Toggle::make('is_locked')
->label(trans('server/backup.actions.create.locked'))
->helperText(trans('server/backup.actions.create.lock_helper'))
->columnSpanFull(),
->label('Lock?')
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
]);
}
@@ -99,93 +98,60 @@ class BackupResource extends Resource
return $table
->columns([
TextColumn::make('name')
->label(trans('server/backup.actions.create.name'))
->searchable(),
BytesColumn::make('bytes')
->label(trans('server/backup.size')),
->label('Size'),
DateTimeColumn::make('created_at')
->label(trans('server/backup.created_at'))
->label('Created')
->since()
->sortable(),
TextColumn::make('status')
->label(trans('server/backup.status'))
->label('Status')
->badge(),
IconColumn::make('is_locked')
->label(trans('server/backup.is_locked'))
->visibleFrom('md')
->label('Lock Status')
->trueIcon('tabler-lock')
->falseIcon('tabler-lock-open'),
])
->actions([
ActionGroup::make([
Action::make('rename')
->icon('tabler-pencil')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label(trans('server/backup.actions.rename.title'))
->form([
TextInput::make('name')
->label(trans('server/backup.actions.rename.new_name'))
->required()
->maxLength(255)
->default(fn (Backup $backup) => $backup->name),
])
->action(function (Backup $backup, $data) {
$oldName = $backup->name;
$newName = $data['name'];
$backup->update(['name' => $newName]);
if ($oldName !== $newName) {
Activity::event('server:backup.rename')
->subject($backup)
->property(['old_name' => $oldName, 'new_name' => $newName])
->log();
}
Notification::make()
->title(trans('server/backup.actions.rename.notification_success'))
->success()
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('lock')
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
->label(fn (Backup $backup) => !$backup->is_locked ? trans('server/backup.actions.lock.lock') : trans('server/backup.actions.lock.unlock'))
->label(fn (Backup $backup) => !$backup->is_locked ? 'Lock' : 'Unlock')
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup))
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('download')
->label(trans('server/backup.actions.download'))
->color('primary')
->icon('tabler-download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DOWNLOAD, $server))
->url(fn (DownloadLinkService $downloadLinkService, Backup $backup, Request $request) => $downloadLinkService->handle($backup, $request->user()), true)
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
Action::make('restore')
->label(trans('server/backup.actions.restore.title'))
->color('success')
->icon('tabler-folder-up')
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_RESTORE, $server))
->form([
Placeholder::make('')
->helperText(trans('server/backup.actions.restore.helper')),
->helperText('Your server will be stopped. You will not be able to control the power state, access the file manager, or create additional backups until this process is completed.'),
Checkbox::make('truncate')
->label(trans('server/backup.actions.restore.delete_all')),
->label('Delete all files before restoring backup?'),
])
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
if (!is_null($server->status)) {
return Notification::make()
->title(trans('server/backup.actions.restore.notification_fail'))
->body(trans('server/backup.actions.restore.notification_fail_body_1'))
->danger()
->title('Backup Restore Failed')
->body('This server is not currently in a state that allows for a backup to be restored.')
->send();
}
if (!$backup->is_successful && is_null($backup->completed_at)) {
return Notification::make()
->title(trans('server/backup.actions.restore.notification_fail'))
->body(trans('server/backup.actions.restore.notification_fail_body_2'))
->danger()
->title('Backup Restore Failed')
->body('This backup cannot be restored at this time: not completed or failed.')
->send();
}
@@ -208,26 +174,21 @@ class BackupResource extends Resource
});
return Notification::make()
->title(trans('server/backup.actions.restore.notification_started'))
->title('Restoring Backup')
->send();
})
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
DeleteAction::make('delete')
->disabled(fn (Backup $backup) => $backup->is_locked)
->modalDescription(fn (Backup $backup) => trans('server/backup.actions.delete.description', ['backup' => $backup->name]))
->modalSubmitActionLabel(trans('server/backup.actions.delete.title'))
->modalDescription(fn (Backup $backup) => 'Do you wish to delete ' . $backup->name . '?')
->modalSubmitActionLabel('Delete Backup')
->action(function (Backup $backup, DeleteBackupService $deleteBackupService) {
try {
$deleteBackupService->handle($backup);
Notification::make()
->title(trans('server/backup.actions.delete.notification_success'))
->success()
->send();
} catch (ConnectionException) {
Notification::make()
->title(trans('server/backup.actions.delete.notification_fail'))
->body(trans('server/backup.actions.delete.notification_fail_body'))
->title('Could not delete backup')
->body('Connection to node failed')
->danger()
->send();
@@ -266,9 +227,4 @@ class BackupResource extends Resource
'index' => Pages\ListBackups::route('/'),
];
}
public static function getNavigationLabel(): string
{
return trans('server/backup.title');
}
}

View File

@@ -15,7 +15,6 @@ use Filament\Actions\CreateAction;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ListBackups extends ListRecords
@@ -34,9 +33,8 @@ class ListBackups extends ListRecords
return [
CreateAction::make()
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
->icon('tabler-file-zip')->iconButton()->iconSize(IconSize::Large)
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
->tooltip(fn () => $server->backups()->count() >= $server->backup_limit ? trans('server/backup.actions.create.limit') : trans('server/backup.actions.create.title'))
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
->createAnother(false)
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
@@ -55,15 +53,15 @@ class ListBackups extends ListRecords
->log();
return Notification::make()
->title(trans('server/backup.actions.create.notification_success'))
->body(trans('server/backup.actions.create.created', ['name' => $backup->name]))
->title('Backup Created')
->body($backup->name . ' created.')
->success()
->send();
} catch (HttpException $e) {
return Notification::make()
->title(trans('server/backup.actions.create.notification_fail'))
->body($e->getMessage() . ' Try again' . ($e->getHeaders()['Retry-After'] ? ' in ' . $e->getHeaders()['Retry-After'] . ' seconds.' : ''))
->danger()
->title('Backup Failed')
->body($e->getMessage() . ' Try again' . ($e->getHeaders()['Retry-After'] ? ' in ' . $e->getHeaders()['Retry-After'] . ' seconds.' : ''))
->send();
}
}),
@@ -74,9 +72,4 @@ class ListBackups extends ListRecords
{
return [];
}
public function getTitle(): string
{
return trans('server/backup.title');
}
}

View File

@@ -66,17 +66,13 @@ class DatabaseResource extends Resource
return $form
->schema([
TextInput::make('host')
->label(trans('server/database.host'))
->formatStateUsing(fn (Database $database) => $database->address())
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('database')
->label(trans('server/database.database'))
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('username')
->label(trans('server/database.username'))
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null),
TextInput::make('password')
->label(trans('server/database.password'))
->password()->revealable()
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->hintAction(
@@ -86,12 +82,11 @@ class DatabaseResource extends Resource
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn (Database $database) => $database->password),
TextInput::make('remote')
->label(trans('server/database.remote')),
->label('Connections From'),
TextInput::make('max_connections')
->label(trans('server/database.max_connections'))
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
TextInput::make('jdbc')
->label(trans('server/database.jdbc'))
->label('JDBC Connection String')
->password()->revealable()
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
@@ -105,22 +100,17 @@ class DatabaseResource extends Resource
return $table
->columns([
TextColumn::make('host')
->label(trans('server/database.host'))
->state(fn (Database $database) => $database->address())
->badge(),
TextColumn::make('database')
->label(trans('server/database.database')),
TextColumn::make('username')
->label(trans('server/database.username')),
TextColumn::make('remote')
->label(trans('server/database.remote')),
TextColumn::make('database'),
TextColumn::make('username'),
TextColumn::make('remote'),
DateTimeColumn::make('created_at')
->label(trans('server/database.created_at'))
->sortable(),
])
->actions([
ViewAction::make()
->modalHeading(fn (Database $database) => trans('server/database.viewing', ['database' => $database->database])),
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
DeleteAction::make()
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
]);
@@ -158,9 +148,4 @@ class DatabaseResource extends Resource
'index' => Pages\ListDatabases::route('/'),
];
}
public static function getNavigationLabel(): string
{
return trans('server/database.title');
}
}

View File

@@ -16,7 +16,6 @@ use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListDatabases extends ListRecords
{
@@ -33,9 +32,7 @@ class ListDatabases extends ListRecords
return [
CreateAction::make('new')
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon(fn () => $server->databases()->count() >= $server->database_limit ? 'tabler-database-x' : 'tabler-database-plus')
->tooltip(fn () => $server->databases()->count() >= $server->database_limit ? trans('server/database.limit') : trans('server/database.create_database'))
->label(fn () => $server->databases()->count() >= $server->database_limit ? 'Database limit reached' : 'Create Database')
->disabled(fn () => $server->databases()->count() >= $server->database_limit)
->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary')
->createAnother(false)
@@ -44,20 +41,20 @@ class ListDatabases extends ListRecords
->columns(2)
->schema([
Select::make('database_host_id')
->label(trans('server/database.database_host'))
->label('Database Host')
->columnSpan(2)
->required()
->placeholder(trans('server/database.database_host_select'))
->placeholder('Select Database Host')
->options(fn () => $server->node->databaseHosts->mapWithKeys(fn (DatabaseHost $databaseHost) => [$databaseHost->id => $databaseHost->name])),
TextInput::make('database')
->label(trans('server/database.name'))
->columnSpan(1)
->label('Database Name')
->prefix('s'. $server->id . '_')
->hintIcon('tabler-question-mark')
->hintIconTooltip(trans('server/database.name_hint')),
->hintIconTooltip('Leaving this blank will auto generate a random name'),
TextInput::make('remote')
->label(trans('server/database.connections_from'))
->columnSpan(1)
->label('Connections From')
->default('%'),
]),
])
@@ -76,9 +73,4 @@ class ListDatabases extends ListRecords
{
return [];
}
public function getTitle(): string
{
return trans('server/database.title');
}
}

View File

@@ -55,9 +55,4 @@ class FileResource extends Resource
'index' => Pages\ListFiles::route('/{path?}'),
];
}
public static function getNavigationLabel(): string
{
return trans('server/file.title');
}
}

View File

@@ -69,10 +69,10 @@ class EditFiles extends Page
return $form
->schema([
Section::make(trans('server/file.actions.edit.title', ['file' => $this->path]))
Section::make('Editing: ' . $this->path)
->footerActions([
Action::make('save_and_close')
->label(trans('server/file.actions.edit.save_close'))
->label('Save & Close')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->icon('tabler-device-floppy')
->keyBindings('mod+shift+s')
@@ -85,14 +85,14 @@ class EditFiles extends Page
Notification::make()
->success()
->title(trans('server/file.actions.edit.notification'))
->title('File saved')
->body(fn () => $this->path)
->send();
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
}),
Action::make('save')
->label(trans('server/file.actions.edit.save'))
->label('Save')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->icon('tabler-device-floppy')
->keyBindings('mod+s')
@@ -105,12 +105,12 @@ class EditFiles extends Page
Notification::make()
->success()
->title(trans('server/file.actions.edit.notification'))
->title('File saved')
->body(fn () => $this->path)
->send();
}),
Action::make('cancel')
->label(trans('server/file.actions.edit.cancel'))
->label('Cancel')
->color('danger')
->icon('tabler-x')
->url(fn () => ListFiles::getUrl(['path' => dirname($this->path)])),
@@ -118,7 +118,7 @@ class EditFiles extends Page
->footerActionsAlignment(Alignment::End)
->schema([
Select::make('lang')
->label(trans('server/file.actions.new_file.syntax'))
->label('Syntax Highlighting')
->searchable()
->native(false)
->live()
@@ -133,25 +133,25 @@ class EditFiles extends Page
try {
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
} catch (FileSizeTooLargeException) {
AlertBanner::make('file_too_large')
->title(trans('server/file.alerts.file_too_large.title', ['name' => basename($this->path)]))
->body(trans('server/file.alerts.file_too_large.body', ['max' => convert_bytes_to_readable(config('panel.files.max_edit_size'))]))
AlertBanner::make()
->title('<code>' . basename($this->path) . '</code> is too large!')
->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
->danger()
->closable()
->send();
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
} catch (FileNotFoundException) {
AlertBanner::make('file_not_found')
->title(trans('server/file.alerts.file_not_found.title', ['name' => basename($this->path)]))
AlertBanner::make()
->title('<code>' . basename($this->path) . '</code> not found!')
->danger()
->closable()
->send();
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
} catch (FileNotEditableException) {
AlertBanner::make('file_is_directory')
->title(trans('server/file.alerts.file_not_found.title', ['name' => basename($this->path)]))
AlertBanner::make()
->title('<code>' . basename($this->path) . '</code> is a directory')
->danger()
->closable()
->send();
@@ -179,11 +179,20 @@ class EditFiles extends Page
if (str($path)->endsWith('.pelicanignore')) {
AlertBanner::make('.pelicanignore_info')
->title(trans('server/file.alerts.pelicanignore.title'))
->body(trans('server/file.alerts.pelicanignore.body'))
->title('You\'re editing a <code>.pelicanignore</code> file!')
->body('Any files or directories listed in here will be excluded from backups. Wildcards are supported by using an asterisk (<code>*</code>).<br>You can negate a prior rule by prepending an exclamation point (<code>!</code>).')
->info()
->closable()
->send();
try {
$this->getDaemonFileRepository()->getDirectory('/');
} catch (ConnectionException) {
AlertBanner::make('node_connection_error')
->title('Could not connect to the node!')
->danger()
->send();
}
}
}

View File

@@ -31,7 +31,6 @@ use Filament\Notifications\Notification;
use Filament\Panel;
use Filament\Resources\Pages\ListRecords;
use Filament\Resources\Pages\PageRegistration;
use Filament\Support\Enums\IconSize;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\BulkAction;
@@ -90,52 +89,49 @@ class ListFiles extends ListRecords
->defaultSort('name')
->columns([
TextColumn::make('name')
->label(trans('server/file.name'))
->searchable()
->sortable()
->icon(fn (File $file) => $file->getIcon()),
BytesColumn::make('size')
->label(trans('server/file.size'))
->visibleFrom('md')
->state(fn (File $file) => $file->is_directory ? null : $file->size)
->sortable(),
DateTimeColumn::make('modified_at')
->label(trans('server/file.modified_at'))
->visibleFrom('md')
->since()
->sortable(),
])
->recordUrl(function (File $file) use ($server) {
if ($file->is_directory) {
return self::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]);
return self::getUrl(['path' => join_paths($this->path, $file->name)]);
}
if (!auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) {
return null;
}
return $file->canEdit() ? EditFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]) : null;
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
})
->actions([
Action::make('view')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->label(trans('server/file.actions.open'))
->label('Open')
->icon('tabler-eye')
->visible(fn (File $file) => $file->is_directory)
->url(fn (File $file) => self::getUrl(['path' => encode_path(join_paths($this->path, $file->name))])),
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
EditAction::make('edit')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->icon('tabler-edit')
->visible(fn (File $file) => $file->canEdit())
->url(fn (File $file) => EditFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))])),
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
ActionGroup::make([
Action::make('rename')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label(trans('server/file.actions.rename.title'))
->label('Rename')
->icon('tabler-forms')
->form([
TextInput::make('name')
->label(trans('server/file.actions.rename.file_name'))
->label('File name')
->default(fn (File $file) => $file->name)
->required(),
])
@@ -152,14 +148,14 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(trans('server/file.actions.rename.notification'))
->title('File Renamed')
->body(fn () => $file->name . ' -> ' . $data['name'])
->success()
->send();
}),
Action::make('copy')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->label(trans('server/file.actions.copy.title'))
->label('Copy')
->icon('tabler-copy')
->visible(fn (File $file) => $file->is_file)
->action(function (File $file) {
@@ -170,7 +166,7 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(trans('server/file.actions.copy.notification'))
->title('File copied')
->success()
->send();
@@ -178,18 +174,18 @@ class ListFiles extends ListRecords
}),
Action::make('download')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
->label(trans('server/file.actions.download'))
->label('Download')
->icon('tabler-download')
->visible(fn (File $file) => $file->is_file)
->url(fn (File $file) => DownloadFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]), true),
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
Action::make('move')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label(trans('server/file.actions.move.title'))
->label('Move')
->icon('tabler-replace')
->form([
TextInput::make('location')
->label(trans('server/file.actions.move.new_location'))
->hint(trans('server/file.actions.move.new_location_hint'))
->label('New location')
->hint('Enter the location of this file or folder, relative to the current directory.')
->required()
->live(),
Placeholder::make('new_location')
@@ -212,24 +208,22 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(trans('server/file.actions.move.notification'))
->title('File Moved')
->body($oldLocation . ' -> ' . $newLocation)
->success()
->send();
}),
Action::make('permissions')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->label(trans('server/file.actions.permissions.title'))
->label('Permissions')
->icon('tabler-license')
->form([
CheckboxList::make('owner')
->label(trans('server/file.actions.permissions.owner'))
->bulkToggleable()
->columns(3)
->options([
'read' => trans('server/file.actions.permissions.read'),
'write' => trans('server/file.actions.permissions.write'),
'execute' => trans('server/file.actions.permissions.execute'),
'read' => 'Read',
'write' => 'Write',
'execute' => 'Execute',
])
->formatStateUsing(function ($state, File $file) {
$mode = (int) substr((string) $file->mode_bits, 0, 1);
@@ -237,13 +231,11 @@ class ListFiles extends ListRecords
return $this->getPermissionsFromModeBit($mode);
}),
CheckboxList::make('group')
->label(trans('server/file.actions.permissions.group'))
->bulkToggleable()
->columns(3)
->options([
'read' => trans('server/file.actions.permissions.read'),
'write' => trans('server/file.actions.permissions.write'),
'execute' => trans('server/file.actions.permissions.execute'),
'read' => 'Read',
'write' => 'Write',
'execute' => 'Execute',
])
->formatStateUsing(function ($state, File $file) {
$mode = (int) substr((string) $file->mode_bits, 1, 1);
@@ -251,13 +243,11 @@ class ListFiles extends ListRecords
return $this->getPermissionsFromModeBit($mode);
}),
CheckboxList::make('public')
->label(trans('server/file.actions.permissions.public'))
->bulkToggleable()
->columns(3)
->options([
'read' => trans('server/file.actions.permissions.read'),
'write' => trans('server/file.actions.permissions.write'),
'execute' => trans('server/file.actions.permissions.execute'),
'read' => 'Read',
'write' => 'Write',
'execute' => 'Execute',
])
->formatStateUsing(function ($state, File $file) {
$mode = (int) substr((string) $file->mode_bits, 2, 1);
@@ -275,17 +265,17 @@ class ListFiles extends ListRecords
$this->getDaemonFileRepository()->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
Notification::make()
->title(trans('server/file.actions.permissions.notification', ['mode' => $mode]))
->title('Permissions changed to ' . $mode)
->success()
->send();
}),
Action::make('archive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->label(trans('server/file.actions.archive.title'))
->label('Archive')
->icon('tabler-archive')
->form([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->label('Archive name')
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
])
@@ -299,7 +289,7 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(trans('server/file.actions.archive.notification'))
->title('Archive created')
->body($archive['name'])
->success()
->send();
@@ -308,7 +298,7 @@ class ListFiles extends ListRecords
}),
Action::make('unarchive')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->label(trans('server/file.actions.unarchive.title'))
->label('Unarchive')
->icon('tabler-archive')
->visible(fn (File $file) => $file->isArchive())
->action(function (File $file) {
@@ -320,7 +310,7 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(trans('server/file.actions.unarchive.notification'))
->title('Unarchive completed')
->success()
->send();
@@ -348,8 +338,8 @@ class ListFiles extends ListRecords
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
->form([
TextInput::make('location')
->label(trans('server/file.actions.move.directory'))
->hint(trans('server/file.actions.move.directory_hint'))
->label('Directory')
->hint('Enter the new directory, relative to the current directory.')
->required()
->live(),
Placeholder::make('new_location')
@@ -367,7 +357,7 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(trans('server/file.actions.move.bulk_notification', ['count' => count($files), 'directory' => resolve_path(join_paths($this->path, $location))]))
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
->success()
->send();
}),
@@ -375,7 +365,7 @@ class ListFiles extends ListRecords
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
->form([
TextInput::make('name')
->label(trans('server/file.actions.archive.archive_name'))
->label('Archive name')
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
->suffix('.tar.gz'),
])
@@ -391,7 +381,7 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(trans('server/file.actions.archive.notification'))
->title('Archive created')
->body($archive['name'])
->success()
->send();
@@ -410,7 +400,7 @@ class ListFiles extends ListRecords
->log();
Notification::make()
->title(trans('server/file.actions.delete.bulk_notification', ['count' => count($files)]))
->title(count($files) . ' Files deleted.')
->success()
->send();
}),
@@ -426,10 +416,10 @@ class ListFiles extends ListRecords
return [
HeaderAction::make('new_file')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->tooltip(trans('server/file.actions.new_file.title'))
->hiddenLabel()->icon('tabler-file-plus')->iconButton()->iconSize(IconSize::Large)
->color('primary')
->modalSubmitActionLabel(trans('server/file.actions.new_file.create'))
->label('New File')
->color('gray')
->keyBindings('')
->modalSubmitActionLabel('Create')
->action(function ($data) {
$path = join_paths($this->path, $data['name']);
try {
@@ -439,8 +429,8 @@ class ListFiles extends ListRecords
->property('file', join_paths($path, $data['name']))
->log();
} catch (FileExistsException) {
AlertBanner::make('file_already_exists')
->title(trans('server/file.alerts.file_already_exists.title', ['name' => $path]))
AlertBanner::make()
->title('<code>' . $path . '</code> already exists!')
->danger()
->closable()
->send();
@@ -450,10 +440,10 @@ class ListFiles extends ListRecords
})
->form([
TextInput::make('name')
->label(trans('server/file.actions.new_file.file_name'))
->label('File Name')
->required(),
Select::make('lang')
->label(trans('server/file.actions.new_file.syntax'))
->label('Syntax Highlighting')
->searchable()
->native(false)
->live()
@@ -468,9 +458,8 @@ class ListFiles extends ListRecords
]),
HeaderAction::make('new_folder')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->hiddenLabel()->icon('tabler-folder-plus')->iconButton()->iconSize(IconSize::Large)
->tooltip(trans('server/file.actions.new_folder.title'))
->color('primary')
->label('New Folder')
->color('gray')
->action(function ($data) {
try {
$this->getDaemonFileRepository()->createDirectory($data['name'], $this->path);
@@ -480,8 +469,8 @@ class ListFiles extends ListRecords
->log();
} catch (FileExistsException) {
$path = join_paths($this->path, $data['name']);
AlertBanner::make('folder_already_exists')
->title(trans('server/file.alerts.file_already_exists.title', ['name' => $path]))
AlertBanner::make()
->title('<code>' . $path . '</code> already exists!')
->danger()
->closable()
->send();
@@ -491,14 +480,12 @@ class ListFiles extends ListRecords
})
->form([
TextInput::make('name')
->label(trans('server/file.actions.new_folder.folder_name'))
->label('Folder Name')
->required(),
]),
HeaderAction::make('upload')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
->hiddenLabel()->icon('tabler-upload')->iconButton()->iconSize(IconSize::Large)
->tooltip(trans('server/file.actions.upload.title'))
->color('success')
->label('Upload')
->action(function ($data) {
if (count($data['files']) > 0 && !isset($data['url'])) {
/** @var UploadedFile $file */
@@ -525,8 +512,7 @@ class ListFiles extends ListRecords
Tabs::make()
->contained(false)
->schema([
Tab::make('from_files')
->label(trans('server/file.actions.upload.from_files'))
Tab::make('Upload Files')
->live()
->schema([
FileUpload::make('files')
@@ -536,29 +522,23 @@ class ListFiles extends ListRecords
->maxSize((int) round($server->node->upload_size * (config('panel.use_binary_prefix') ? 1.048576 * 1024 : 1000)))
->multiple(),
]),
Tab::make('url')
->label(trans('server/file.actions.upload.url'))
Tab::make('Upload From URL')
->live()
->disabled(fn (Get $get) => count($get('files')) > 0)
->schema([
TextInput::make('url')
->label(trans('server/file.actions.upload.url'))
->label('URL')
->url(),
]),
]),
]),
HeaderAction::make('search')
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->tooltip(trans('server/file.actions.global_search.title'))
->color('primary')
->icon('tabler-world-search')
->modalHeading(trans('server/file.actions.global_search.title'))
->modalSubmitActionLabel(trans('server/file.actions.global_search.search'))
->label('Global Search')
->modalSubmitActionLabel('Search')
->form([
TextInput::make('searchTerm')
->label(trans('server/file.actions.global_search.search_term'))
->placeholder(trans('server/file.actions.global_search.search_term_placeholder'))
->placeholder('Enter a search term, e.g. *.txt')
->required()
->regex('/^[^*]*\*?[^*]*$/')
->minValue(3),
@@ -606,9 +586,4 @@ class ListFiles extends ListRecords
->where('path', '.*'),
);
}
public function getTitle(): string
{
return trans('server/file.title');
}
}

View File

@@ -13,7 +13,6 @@ use Filament\Facades\Filament;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Contracts\Support\Htmlable;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
@@ -24,6 +23,8 @@ class SearchFiles extends ListRecords
protected static string $resource = FileResource::class;
protected static ?string $title = 'Global Search';
#[Locked]
public string $searchTerm;
@@ -36,7 +37,7 @@ class SearchFiles extends ListRecords
return [
$resource::getUrl() => $resource::getBreadcrumb(),
self::getUrl(['searchTerm' => $this->searchTerm]) => trans('server/file.actions.global_search.search_for_term', ['term' => ' "' . $this->searchTerm . '"']),
self::getUrl(['searchTerm' => $this->searchTerm]) => 'Search "' . $this->searchTerm . '"',
];
}
@@ -50,18 +51,10 @@ class SearchFiles extends ListRecords
->query(fn () => File::get($server, $this->path, $this->searchTerm)->orderByDesc('is_directory')->orderBy('name'))
->columns([
TextColumn::make('name')
->label(trans('server/file.name'))
->searchable()
->sortable()
->icon(fn (File $file) => $file->getIcon()),
BytesColumn::make('size')
->label(trans('server/file.size'))
->visibleFrom('md')
->state(fn (File $file) => $file->size)
->sortable(),
BytesColumn::make('size'),
DateTimeColumn::make('modified_at')
->label(trans('server/file.modified_at'))
->visibleFrom('md')
->since()
->sortable(),
])
@@ -73,9 +66,4 @@ class SearchFiles extends ListRecords
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
});
}
public function getTitle(): string|Htmlable
{
return trans('server/file.actions.global_search.title');
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Filament\Server\Resources;
use App\Facades\Activity;
use App\Filament\Components\Forms\Actions\CronPresetAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
@@ -19,14 +18,13 @@ use Carbon\Carbon;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\PageRegistration;
@@ -40,7 +38,6 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString;
class ScheduleResource extends Resource
{
@@ -80,82 +77,147 @@ class ScheduleResource extends Resource
{
return $form
->columns([
'default' => 1,
'lg' => 2,
'default' => 4,
'lg' => 5,
])
->schema([
TextInput::make('name')
->label(trans('server/schedule.name'))
->columnSpanFull()
->columnSpan([
'default' => 4,
'md' => 3,
'lg' => 4,
])
->label('Schedule Name')
->placeholder('A human readable identifier for this schedule.')
->autocomplete(false)
->required(),
Toggle::make('only_when_online')
->label(trans('server/schedule.only_online'))
->hintIconTooltip(trans('server/schedule.only_online_hint'))
->hintIcon('tabler-question-mark')
->inline(false)
->required()
->default(1),
Toggle::make('is_active')
->label(trans('server/schedule.enabled'))
->hintIconTooltip(trans('server/schedule.enabled_hint'))
->hintIcon('tabler-question-mark')
->inline(false)
->hiddenOn('view')
->required()
->default(1),
ToggleButtons::make('Status')
->formatStateUsing(fn (Schedule $schedule) => !$schedule->is_active ? 'inactive' : ($schedule->is_processing ? 'processing' : 'active'))
->options(fn (Schedule $schedule) => !$schedule->is_active ? ['inactive' => trans('server/schedule.inactive')] : ($schedule->is_processing ? ['processing' => trans('server/schedule.processing')] : ['active' => trans('server/schedule.active')]))
->options(fn (Schedule $schedule) => !$schedule->is_active ? ['inactive' => 'Inactive'] : ($schedule->is_processing ? ['processing' => 'Processing'] : ['active' => 'Active']))
->colors([
'inactive' => 'danger',
'processing' => 'warning',
'active' => 'success',
])
->visibleOn('view'),
Section::make('Cron')
->label(trans('server/schedule.cron'))
->description(function (Get $get) {
try {
$nextRun = Utilities::getScheduleNextRunDate($get('cron_minute'), $get('cron_hour'), $get('cron_day_of_month'), $get('cron_month'), $get('cron_day_of_week'))->timezone(auth()->user()->timezone);
} catch (Exception) {
$nextRun = trans('server/schedule.invalid');
}
return new HtmlString(trans('server/schedule.cron_body') . '<br>' . trans('server/schedule.cron_timezone', ['timezone' => auth()->user()->timezone, 'next_run' => $nextRun]));
})
->visibleOn('view')
->columnSpan([
'default' => 4,
'md' => 1,
'lg' => 1,
]),
Toggle::make('only_when_online')
->label('Only when Server is Online?')
->hintIconTooltip('Only execute this schedule when the server is in a running state.')
->hintIcon('tabler-question-mark')
->inline(false)
->columnSpan([
'default' => 2,
'lg' => 3,
])
->required()
->default(1),
Toggle::make('is_active')
->label('Enable Schedule?')
->hintIconTooltip('This schedule will be executed automatically if enabled.')
->hintIcon('tabler-question-mark')
->inline(false)
->columnSpan([
'default' => 2,
'lg' => 2,
])
->required()
->default(1),
TextInput::make('cron_minute')
->columnSpan([
'default' => 2,
'lg' => 1,
])
->label('Minute')
->default('*/5')
->required(),
TextInput::make('cron_hour')
->columnSpan([
'default' => 2,
'lg' => 1,
])
->label('Hour')
->default('*')
->required(),
TextInput::make('cron_day_of_month')
->columnSpan([
'default' => 2,
'lg' => 1,
])
->label('Day of Month')
->default('*')
->required(),
TextInput::make('cron_month')
->columnSpan([
'default' => 2,
'lg' => 1,
])
->label('Month')
->default('*')
->required(),
TextInput::make('cron_day_of_week')
->columnSpan([
'default' => 2,
'lg' => 1,
])
->label('Day of Week')
->default('*')
->required(),
Section::make('Presets')
->hiddenOn('view')
->columns(1)
->schema([
Actions::make([
CronPresetAction::make('hourly')
->label(trans('server/schedule.time.hourly'))
->cron('0', '*', '*', '*', '*'),
CronPresetAction::make('daily')
->label(trans('server/schedule.time.daily'))
->cron('0', '0', '*', '*', '*'),
CronPresetAction::make('weekly_monday')
->label(trans('server/schedule.time.weekly_mon'))
->cron('0', '0', '*', '*', '1'),
CronPresetAction::make('weekly_sunday')
->label(trans('server/schedule.time.weekly_sun'))
->cron('0', '0', '*', '*', '0'),
CronPresetAction::make('monthly')
->label(trans('server/schedule.time.monthly'))
->cron('0', '0', '1', '*', '*'),
CronPresetAction::make('every_x_minutes')
->label(trans('server/schedule.time.every_min'))
->color(fn (Get $get) => str($get('cron_minute'))->startsWith('*/')
&& $get('cron_hour') == '*'
&& $get('cron_day_of_month') == '*'
&& $get('cron_month') == '*'
&& $get('cron_day_of_week') == '*' ? 'success' : 'primary')
Action::make('hourly')
->disabled(fn (string $operation) => $operation === 'view')
->action(function (Set $set) {
$set('cron_minute', '0');
$set('cron_hour', '*');
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
Action::make('daily')
->disabled(fn (string $operation) => $operation === 'view')
->action(function (Set $set) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
Action::make('weekly')
->disabled(fn (string $operation) => $operation === 'view')
->action(function (Set $set) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '*');
$set('cron_month', '*');
$set('cron_day_of_week', '0');
}),
Action::make('monthly')
->disabled(fn (string $operation) => $operation === 'view')
->action(function (Set $set) {
$set('cron_minute', '0');
$set('cron_hour', '0');
$set('cron_day_of_month', '1');
$set('cron_month', '*');
$set('cron_day_of_week', '0');
}),
Action::make('every_x_minutes')
->disabled(fn (string $operation) => $operation === 'view')
->form([
TextInput::make('x')
->label('')
->numeric()
->minValue(1)
->maxValue(60)
->prefix(trans('server/schedule.time.every'))
->suffix(trans('server/schedule.time.minutes')),
->prefix('Every')
->suffix('Minutes'),
])
->action(function (Set $set, $data) {
$set('cron_minute', '*/' . $data['x']);
@@ -164,20 +226,16 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_hours')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& str($get('cron_hour'))->startsWith('*/')
&& $get('cron_day_of_month') == '*'
&& $get('cron_month') == '*'
&& $get('cron_day_of_week') == '*' ? 'success' : 'primary')
Action::make('every_x_hours')
->disabled(fn (string $operation) => $operation === 'view')
->form([
TextInput::make('x')
->label('')
->numeric()
->minValue(1)
->maxValue(24)
->prefix(trans('server/schedule.time.every'))
->suffix(trans('server/schedule.time.hours')),
->prefix('Every')
->suffix('Hours'),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');
@@ -186,20 +244,16 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_days')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& str($get('cron_day_of_month'))->startsWith('*/')
&& $get('cron_month') == '*'
&& $get('cron_day_of_week') == '*' ? 'success' : 'primary')
Action::make('every_x_days')
->disabled(fn (string $operation) => $operation === 'view')
->form([
TextInput::make('x')
->label('')
->numeric()
->minValue(1)
->maxValue(24)
->prefix(trans('server/schedule.time.every'))
->suffix(trans('server/schedule.time.days')),
->prefix('Every')
->suffix('Days'),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');
@@ -208,20 +262,16 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_months')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& $get('cron_day_of_month') == '1'
&& str($get('cron_month'))->startsWith('*/')
&& $get('cron_day_of_week') == '*' ? 'success' : 'primary')
Action::make('every_x_months')
->disabled(fn (string $operation) => $operation === 'view')
->form([
TextInput::make('x')
->label('')
->numeric()
->minValue(1)
->maxValue(24)
->prefix(trans('server/schedule.time.every'))
->suffix(trans('server/schedule.time.months')),
->prefix('Every')
->suffix('Months'),
])
->action(function (Set $set, $data) {
$set('cron_minute', '0');
@@ -230,24 +280,20 @@ class ScheduleResource extends Resource
$set('cron_month', '*/' . $data['x']);
$set('cron_day_of_week', '*');
}),
CronPresetAction::make('every_x_day_of_week')
->color(fn (Get $get) => $get('cron_minute') == '0'
&& $get('cron_hour') == '0'
&& $get('cron_day_of_month') == '*'
&& $get('cron_month') == '*'
&& $get('cron_day_of_week') != '*' ? 'success' : 'primary')
Action::make('every_x_day_of_week')
->disabled(fn (string $operation) => $operation === 'view')
->form([
Select::make('x')
->label('')
->prefix(trans('server/schedule.time.every'))
->prefix('Every')
->options([
'1' => trans('server/schedule.time.monday'),
'2' => trans('server/schedule.time.tuesday'),
'3' => trans('server/schedule.time.wednesday'),
'4' => trans('server/schedule.time.thursday'),
'5' => trans('server/schedule.time.friday'),
'6' => trans('server/schedule.time.saturday'),
'0' => trans('server/schedule.time.sunday'),
'1' => 'Monday',
'2' => 'Tuesday',
'3' => 'Wednesday',
'4' => 'Thursday',
'5' => 'Friday',
'6' => 'Saturday',
'0' => 'Sunday',
])
->selectablePlaceholder(false)
->native(false),
@@ -259,59 +305,7 @@ class ScheduleResource extends Resource
$set('cron_month', '*');
$set('cron_day_of_week', $data['x']);
}),
])
->hiddenOn('view'),
Group::make([
TextInput::make('cron_minute')
->label(trans('server/schedule.time.minute'))
->columnSpan([
'default' => 2,
'lg' => 1,
])
->default('*/5')
->required()
->live(),
TextInput::make('cron_hour')
->label(trans('server/schedule.time.hour'))
->columnSpan([
'default' => 2,
'lg' => 1,
])
->default('*')
->required()
->live(),
TextInput::make('cron_day_of_month')
->label(trans('server/schedule.time.day_of_month'))
->columnSpan([
'default' => 2,
'lg' => 1,
])
->default('*')
->required()
->live(),
TextInput::make('cron_month')
->label(trans('server/schedule.time.month'))
->columnSpan([
'default' => 2,
'lg' => 1,
])
->default('*')
->required()
->live(),
TextInput::make('cron_day_of_week')
->label(trans('server/schedule.time.day_of_week'))
->columnSpan([
'default' => 2,
'lg' => 1,
])
->default('*')
->required()
->live(),
])
->columns([
'default' => 4,
'lg' => 5,
]),
]),
]),
]);
}
@@ -321,26 +315,22 @@ class ScheduleResource extends Resource
return $table
->columns([
TextColumn::make('name')
->label(trans('server/schedule.name'))
->searchable(),
TextColumn::make('cron')
->label(trans('server/schedule.cron'))
->state(fn (Schedule $schedule) => $schedule->cron_minute . ' ' . $schedule->cron_hour . ' ' . $schedule->cron_day_of_month . ' ' . $schedule->cron_month . ' ' . $schedule->cron_day_of_week),
TextColumn::make('status')
->label(trans('server/schedule.status'))
->state(fn (Schedule $schedule) => !$schedule->is_active ? trans('server/schedule.inactive') : ($schedule->is_processing ? trans('server/schedule.processing') : trans('server/schedule.active'))),
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
IconColumn::make('only_when_online')
->label(trans('server/schedule.online_only'))
->boolean()
->sortable(),
DateTimeColumn::make('last_run_at')
->label(trans('server/schedule.last_run'))
->placeholder(trans('server/schedule.never'))
->label('Last run')
->placeholder('Never')
->since()
->sortable(),
DateTimeColumn::make('next_run_at')
->label(trans('server/schedule.next_run'))
->placeholder(trans('server/schedule.never'))
->label('Next run')
->placeholder('Never')
->since()
->sortable()
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
@@ -383,16 +373,11 @@ class ScheduleResource extends Resource
return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek);
} catch (Exception) {
Notification::make()
->title(trans('server/schedule.notification_invalid_cron'))
->title('The cron data provided does not evaluate to a valid expression')
->danger()
->send();
throw new Halt();
}
}
public static function getNavigationLabel(): string
{
return trans('server/schedule.title');
}
}

View File

@@ -3,14 +3,12 @@
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Facades\Activity;
use App\Filament\Components\Actions\ExportScheduleAction;
use App\Filament\Server\Resources\ScheduleResource;
use App\Models\Schedule;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\IconSize;
class EditSchedule extends EditRecord
{
@@ -47,26 +45,13 @@ class EditSchedule extends EditRecord
{
return [
Actions\DeleteAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon('tabler-trash')
->tooltip(trans('server/schedule.delete'))
->after(function ($record) {
Activity::event('server:schedule.delete')
->property('name', $record->name)
->log();
}),
ExportScheduleAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon('tabler-download')
->tooltip(trans('server/schedule.export')),
$this->getSaveFormAction()->formId('form')
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon('tabler-device-floppy')
->tooltip(trans('server/schedule.save')),
$this->getCancelFormAction()->formId('form')
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon('tabler-cancel')
->tooltip(trans('server/schedule.cancel')),
$this->getSaveFormAction()->formId('form')->label('Save'),
$this->getCancelFormAction()->formId('form'),
];
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
use App\Filament\Components\Actions\ImportScheduleAction;
use App\Filament\Server\Resources\ScheduleResource;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
@@ -10,7 +9,6 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
class ListSchedules extends ListRecords
{
@@ -24,13 +22,7 @@ class ListSchedules extends ListRecords
{
return [
CreateAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon('tabler-calendar-plus')
->tooltip(trans('server/schedule.new')),
ImportScheduleAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon('tabler-download')
->tooltip(trans('server/schedule.import')),
->label('New Schedule'),
];
}
@@ -38,9 +30,4 @@ class ListSchedules extends ListRecords
{
return [];
}
public function getTitle(): string
{
return trans('server/schedule.title');
}
}

View File

@@ -14,7 +14,6 @@ use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Facades\Filament;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\IconSize;
class ViewSchedule extends ViewRecord
{
@@ -29,7 +28,7 @@ class ViewSchedule extends ViewRecord
return [
Action::make('runNow')
->authorize(fn () => auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant()))
->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? trans('server/schedule.no_tasks') : ($schedule->is_processing ? trans('server/schedule.processing') : trans('server/schedule.run_now')))
->label(fn (Schedule $schedule) => $schedule->tasks->count() === 0 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now'))
->color(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing ? 'warning' : 'primary')
->disabled(fn (Schedule $schedule) => $schedule->tasks->count() === 0 || $schedule->is_processing)
->action(function (ProcessScheduleService $service, Schedule $schedule) {
@@ -42,10 +41,7 @@ class ViewSchedule extends ViewRecord
$this->fillForm();
}),
EditAction::make()
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon('tabler-calendar-code')
->tooltip(trans('server/schedule.edit')),
EditAction::make(),
];
}

View File

@@ -30,10 +30,10 @@ class TasksRelationManager extends RelationManager
private function getActionOptions(bool $full = true): array
{
return [
Task::ACTION_POWER => $full ? trans('server/schedule.tasks.actions.power.title') : trans('server/schedule.tasks.actions.power.action'),
Task::ACTION_COMMAND => $full ? trans('server/schedule.tasks.actions.command.title') : trans('server/schedule.tasks.actions.command.command'),
Task::ACTION_BACKUP => $full ? trans('server/schedule.tasks.actions.backup.title') : trans('server/schedule.tasks.actions.backup.files_to_ignore'),
Task::ACTION_DELETE_FILES => $full ? trans('server/schedule.tasks.actions.delete.title') : trans('server/schedule.tasks.actions.delete.files_to_delete'),
Task::ACTION_POWER => $full ? 'Send power action' : 'Power action',
Task::ACTION_COMMAND => $full ? 'Send command' : 'Command',
Task::ACTION_BACKUP => $full ? 'Create backup' : 'Files to ignore',
Task::ACTION_DELETE_FILES => $full ? 'Delete files' : 'Files to delete',
];
}
@@ -44,7 +44,6 @@ class TasksRelationManager extends RelationManager
{
return [
Select::make('action')
->label(trans('server/schedule.tasks.actions.title'))
->required()
->live()
->disableOptionWhen(fn (string $value) => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0)
@@ -54,29 +53,27 @@ class TasksRelationManager extends RelationManager
->afterStateUpdated(fn ($state, Set $set) => $set('payload', $state === Task::ACTION_POWER ? 'restart' : null)),
Textarea::make('payload')
->hidden(fn (Get $get) => $get('action') === Task::ACTION_POWER)
->label(fn (Get $get) => $this->getActionOptions(false)[$get('action')] ?? trans('server/schedule.tasks.payload')),
->label(fn (Get $get) => $this->getActionOptions(false)[$get('action')] ?? 'Payload'),
Select::make('payload')
->visible(fn (Get $get) => $get('action') === Task::ACTION_POWER)
->label(trans('server/schedule.tasks.actions.power.action'))
->label('Power Action')
->required()
->options([
'start' => trans('server/schedule.tasks.actions.power.start'),
'restart' => trans('server/schedule.tasks.actions.power.restart'),
'stop' => trans('server/schedule.tasks.actions.power.stop'),
'kill' => trans('server/schedule.tasks.actions.power.kill'),
'start' => 'Start',
'restart' => 'Restart',
'stop' => 'Stop',
'kill' => 'Kill',
])
->selectablePlaceholder(false)
->default('restart'),
TextInput::make('time_offset')
->label(trans('server/schedule.tasks.time_offset'))
->hidden(fn (Get $get) => config('queue.default') === 'sync' || $get('sequence_id') === 1)
->default(0)
->numeric()
->minValue(0)
->maxValue(900)
->suffix(trans('server/schedule.tasks.seconds')),
Toggle::make('continue_on_failure')
->label(trans('server/schedule.tasks.continue_on_failure')),
->suffix('Seconds'),
Toggle::make('continue_on_failure'),
];
}
@@ -90,21 +87,17 @@ class TasksRelationManager extends RelationManager
->defaultSort('sequence_id')
->columns([
TextColumn::make('action')
->label(trans('server/schedule.tasks.actions.title'))
->state(fn (Task $task) => $this->getActionOptions()[$task->action] ?? $task->action),
TextColumn::make('payload')
->label(trans('server/schedule.tasks.payload'))
->state(fn (Task $task) => match ($task->payload) {
'start', 'restart', 'stop', 'kill' => mb_ucfirst($task->payload),
default => explode(PHP_EOL, $task->payload)
})
->badge(),
TextColumn::make('time_offset')
->label(trans('server/schedule.tasks.time_offset'))
->hidden(fn () => config('queue.default') === 'sync')
->suffix(' '. trans('server/schedule.tasks.seconds')),
->suffix(' Seconds'),
IconColumn::make('continue_on_failure')
->label(trans('server/schedule.tasks.continue_on_failure'))
->boolean(),
])
->actions([
@@ -140,7 +133,7 @@ class TasksRelationManager extends RelationManager
->headerActions([
CreateAction::make()
->createAnother(false)
->label(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10) ? trans('server/schedule.tasks.limit') : trans('server/schedule.tasks.create'))
->label(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10) ? 'Task Limit Reached' : 'Create Task')
->disabled(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10))
->form($this->getTaskForm($schedule))
->action(function ($data) use ($schedule) {

View File

@@ -91,15 +91,14 @@ class UserResource extends Resource
foreach ($data['permissions'] as $permission) {
$options[$permission] = str($permission)->headline();
$descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make($data['name'])
->label(str($data['name'])->headline())
$tabs[] = Tab::make(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/user.permissions.' . $data['name'] . '_desc'))
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
->icon($data['icon'])
->schema([
CheckboxList::make($data['name'])
@@ -122,33 +121,30 @@ class UserResource extends Resource
->alignCenter()->circular()
->defaultImageUrl(fn (User $user) => Filament::getUserAvatarUrl($user)),
TextColumn::make('username')
->label(trans('server/user.username'))
->searchable(),
TextColumn::make('email')
->label(trans('server/user.email'))
->searchable(),
TextColumn::make('permissions')
->label(trans('server/user.permissions.title'))
->state(fn (User $user) => count($server->subusers->where('user_id', $user->id)->first()->permissions)),
])
->actions([
DeleteAction::make()
->label(trans('server/user.delete'))
->label('Remove User')
->hidden(fn (User $user) => auth()->user()->id === $user->id)
->action(function (User $user, SubuserDeletionService $subuserDeletionService) use ($server) {
$subuser = $server->subusers->where('user_id', $user->id)->first();
$subuserDeletionService->handle($subuser, $server);
Notification::make()
->title(trans('server/user.notification_delete'))
->title('User Deleted!')
->success()
->send();
}),
EditAction::make()
->label(trans('server/user.edit'))
->label('Edit User')
->hidden(fn (User $user) => auth()->user()->id === $user->id)
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_UPDATE, $server))
->modalHeading(fn (User $user) => trans('server/user.editing', ['user' => $user->email]))
->modalHeading(fn (User $user) => 'Editing ' . $user->email)
->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) {
$subuser = $server->subusers->where('user_id', $user->id)->first();
@@ -162,7 +158,7 @@ class UserResource extends Resource
$subuserUpdateService->handle($subuser, $server, $permissions);
Notification::make()
->title(trans('server/user.notification_edit'))
->title('User Updated!')
->success()
->send();
@@ -189,7 +185,7 @@ class UserResource extends Resource
]),
Actions::make([
Action::make('assignAll')
->label(trans('server/user.assign_all'))
->label('Assign All')
->action(function (Set $set) use ($permissionsArray) {
$permissions = $permissionsArray;
foreach ($permissions as $key => $value) {
@@ -235,9 +231,4 @@ class UserResource extends Resource
'index' => Pages\ListUsers::route('/'),
];
}
public static function getNavigationLabel(): string
{
return trans('server/user.title');
}
}

View File

@@ -24,8 +24,6 @@ use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Illuminate\Contracts\Support\Htmlable;
class ListUsers extends ListRecords
{
@@ -49,15 +47,14 @@ class ListUsers extends ListRecords
foreach ($data['permissions'] as $permission) {
$options[$permission] = str($permission)->headline();
$descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
$permissionsArray[$data['name']][] = $permission;
}
$tabs[] = Tab::make($data['name'])
->label(str($data['name'])->headline())
$tabs[] = Tab::make(str($data['name'])->headline())
->schema([
Section::make()
->description(trans('server/user.permissions.' . $data['name'] . '_desc'))
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
->icon($data['icon'])
->schema([
CheckboxList::make($data['name'])
@@ -72,9 +69,7 @@ class ListUsers extends ListRecords
return [
Actions\CreateAction::make('invite')
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
->icon('tabler-user-plus')
->tooltip(trans('server/user.invite_user'))
->label('Invite User')
->createAnother(false)
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_CREATE, $server))
->form([
@@ -88,7 +83,6 @@ class ListUsers extends ListRecords
])
->schema([
TextInput::make('email')
->label(trans('server/user.email'))
->email()
->inlineLabel()
->columnSpan([
@@ -100,7 +94,7 @@ class ListUsers extends ListRecords
->required(),
assignAll::make([
Action::make('assignAll')
->label(trans('server/user.assign_all'))
->label('Assign All')
->action(function (Set $set, Get $get) use ($permissionsArray) {
$permissions = $permissionsArray;
foreach ($permissions as $key => $value) {
@@ -120,8 +114,8 @@ class ListUsers extends ListRecords
->schema($tabs),
]),
])
->modalHeading(trans('server/user.invite_user'))
->modalSubmitActionLabel(trans('server/user.action'))
->modalHeading('Invite User')
->modalSubmitActionLabel('Invite')
->action(function (array $data, SubuserCreationService $service) use ($server) {
$email = strtolower($data['email']);
@@ -143,12 +137,12 @@ class ListUsers extends ListRecords
]);
Notification::make()
->title(trans('server/user.notification_add'))
->title('User Invited!')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title(trans('server/user.notification_failed'))
->title('Failed')
->body($exception->getMessage())
->danger()
->send();
@@ -163,9 +157,4 @@ class ListUsers extends ListRecords
{
return [];
}
public function getTitle(): string|Htmlable
{
return trans('server/user.title');
}
}

View File

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

View File

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

View File

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

View File

@@ -112,6 +112,6 @@ class ServerNetworkChart extends ChartWidget
{
$lastData = collect(cache()->get("servers.{$this->server->id}.network"))->last();
return trans('server/console.labels.network') . ' - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0);
return 'Network - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0);
}
}

View File

@@ -8,6 +8,7 @@ use App\Models\Server;
use Carbon\CarbonInterface;
use Filament\Notifications\Notification;
use Filament\Widgets\StatsOverviewWidget;
use Illuminate\Support\Number;
use Livewire\Attributes\On;
class ServerOverview extends StatsOverviewWidget
@@ -19,14 +20,14 @@ class ServerOverview extends StatsOverviewWidget
protected function getStats(): array
{
return [
SmallStatBlock::make(trans('server/console.labels.name'), $this->server->name)
SmallStatBlock::make('Name', $this->server->name)
->copyOnClick(fn () => request()->isSecure()),
SmallStatBlock::make(trans('server/console.labels.status'), $this->status()),
SmallStatBlock::make(trans('server/console.labels.address'), $this->server?->allocation->address ?? 'None')
SmallStatBlock::make('Status', $this->status()),
SmallStatBlock::make('Address', $this->server?->allocation->address ?? 'None')
->copyOnClick(fn () => request()->isSecure()),
SmallStatBlock::make(trans('server/console.labels.cpu'), $this->cpuUsage()),
SmallStatBlock::make(trans('server/console.labels.memory'), $this->memoryUsage()),
SmallStatBlock::make(trans('server/console.labels.disk'), $this->diskUsage()),
SmallStatBlock::make('CPU', $this->cpuUsage()),
SmallStatBlock::make('Memory', $this->memoryUsage()),
SmallStatBlock::make('Disk', $this->diskUsage()),
];
}
@@ -53,9 +54,9 @@ class ServerOverview extends StatsOverviewWidget
}
$data = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))->last(default: 0);
$cpu = format_number($data, maxPrecision: 2) . ' %';
$cpu = Number::format($data, maxPrecision: 2, locale: auth()->user()->language) . ' %';
return $cpu . ($this->server->cpu > 0 ? ' / ' . format_number($this->server->cpu) . ' %' : ' / ∞');
return $cpu . ($this->server->cpu > 0 ? ' / ' . Number::format($this->server->cpu, locale: auth()->user()->language) . ' %' : ' / ∞');
}
public function memoryUsage(): string
@@ -67,7 +68,7 @@ class ServerOverview extends StatsOverviewWidget
}
$latestMemoryUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->last(default: 0);
$totalMemory = $this->server->memory * (config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000);
$totalMemory = $this->server->memory * 2 ** 20;
$used = convert_bytes_to_readable($latestMemoryUsed);
$total = convert_bytes_to_readable($totalMemory);
@@ -97,7 +98,7 @@ class ServerOverview extends StatsOverviewWidget
$this->js("window.navigator.clipboard.writeText('{$value}');");
Notification::make()
->title(trans('server/dashboard.copied'))
->title('Copied to clipboard')
->body($value)
->success()
->send();

View File

@@ -36,7 +36,7 @@ class DatabaseHostController extends ApplicationApiController
*/
public function index(GetDatabaseHostRequest $request): array
{
$databases = QueryBuilder::for(DatabaseHost::class)
$databases = QueryBuilder::for(DatabaseHost::query())
->allowedFilters(['name', 'host'])
->allowedSorts(['id', 'name', 'host'])
->paginate($request->query('per_page') ?? 10);

View File

@@ -14,12 +14,6 @@ use App\Http\Requests\Api\Application\Mounts\StoreMountRequest;
use App\Http\Requests\Api\Application\Mounts\DeleteMountRequest;
use App\Http\Requests\Api\Application\Mounts\UpdateMountRequest;
use App\Exceptions\Service\HasActiveServersException;
use App\Http\Requests\Api\Application\Eggs\GetEggsRequest;
use App\Http\Requests\Api\Application\Nodes\GetNodesRequest;
use App\Http\Requests\Api\Application\Servers\GetServerRequest;
use App\Transformers\Api\Application\EggTransformer;
use App\Transformers\Api\Application\NodeTransformer;
use App\Transformers\Api\Application\ServerTransformer;
class MountController extends ApplicationApiController
{
@@ -32,7 +26,7 @@ class MountController extends ApplicationApiController
*/
public function index(GetMountRequest $request): array
{
$mounts = QueryBuilder::for(Mount::class)
$mounts = QueryBuilder::for(Mount::query())
->allowedFilters(['uuid', 'name'])
->allowedSorts(['id', 'uuid'])
->paginate($request->query('per_page') ?? 50);
@@ -119,42 +113,6 @@ class MountController extends ApplicationApiController
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* List assigned eggs
*
* @return array<array-key, mixed>
*/
public function getEggs(GetEggsRequest $request, Mount $mount): array
{
return $this->fractal->collection($mount->eggs)
->transformWith($this->getTransformer(EggTransformer::class))
->toArray();
}
/**
* List assigned nodes
*
* @return array<array-key, mixed>
*/
public function getNodes(GetNodesRequest $request, Mount $mount): array
{
return $this->fractal->collection($mount->nodes)
->transformWith($this->getTransformer(NodeTransformer::class))
->toArray();
}
/**
* List assigned servers
*
* @return array<array-key, mixed>
*/
public function getServers(GetServerRequest $request, Mount $mount): array
{
return $this->fractal->collection($mount->servers)
->transformWith($this->getTransformer(ServerTransformer::class))
->toArray();
}
/**
* Assign eggs to mount
*
@@ -165,11 +123,13 @@ class MountController extends ApplicationApiController
public function addEggs(Request $request, Mount $mount): array
{
$validatedData = $request->validate([
'eggs' => 'required|array|exists:eggs,id',
'eggs.*' => 'integer',
'eggs' => 'required|exists:eggs,id',
]);
$mount->eggs()->attach($validatedData['eggs']);
$eggs = $validatedData['eggs'] ?? [];
if (count($eggs) > 0) {
$mount->eggs()->attach($eggs);
}
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
@@ -177,7 +137,7 @@ class MountController extends ApplicationApiController
}
/**
* Assign nodes to mount
* Assign mounts to mount
*
* Adds nodes to the mount's many-to-many relation.
*
@@ -185,33 +145,12 @@ class MountController extends ApplicationApiController
*/
public function addNodes(Request $request, Mount $mount): array
{
$validatedData = $request->validate([
'nodes' => 'required|array|exists:nodes,id',
'nodes.*' => 'integer',
]);
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
$mount->nodes()->attach($validatedData['nodes']);
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
->toArray();
}
/**
* Assign servers to mount
*
* Adds servers to the mount's many-to-many relation.
*
* @return array<array-key, mixed>
*/
public function addServers(Request $request, Mount $mount): array
{
$validatedData = $request->validate([
'servers' => 'required|array|exists:servers,id',
'servers.*' => 'integer',
]);
$mount->servers()->attach($validatedData['servers']);
$nodes = $data['nodes'] ?? [];
if (count($nodes) > 0) {
$mount->nodes()->attach($nodes);
}
return $this->fractal->item($mount)
->transformWith($this->getTransformer(MountTransformer::class))
@@ -241,16 +180,4 @@ class MountController extends ApplicationApiController
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Unassign server from mount
*
* Deletes a server from the mount's many-to-many relation.
*/
public function deleteServer(Mount $mount, int $server_id): JsonResponse
{
$mount->servers()->detach($server_id);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
}

View File

@@ -39,7 +39,7 @@ class NodeController extends ApplicationApiController
*/
public function index(GetNodesRequest $request): array
{
$nodes = QueryBuilder::for(Node::class)
$nodes = QueryBuilder::for(Node::query())
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id'])
->allowedSorts(['id', 'uuid', 'memory', 'disk', 'cpu'])
->paginate($request->query('per_page') ?? 50);

View File

@@ -25,7 +25,7 @@ class RoleController extends ApplicationApiController
*/
public function index(GetRoleRequest $request): array
{
$roles = QueryBuilder::for(Role::class)
$roles = QueryBuilder::for(Role::query())
->allowedFilters(['id', 'name'])
->allowedSorts(['id', 'name'])
->paginate($request->query('per_page') ?? 10);

View File

@@ -38,7 +38,7 @@ class ServerController extends ApplicationApiController
*/
public function index(GetServersRequest $request): array
{
$servers = QueryBuilder::for(Server::class)
$servers = QueryBuilder::for(Server::query())
->allowedFilters(['uuid', 'uuid_short', 'name', 'description', 'image', 'external_id'])
->allowedSorts(['id', 'uuid'])
->paginate($request->query('per_page') ?? 50);

View File

@@ -41,7 +41,7 @@ class UserController extends ApplicationApiController
*/
public function index(GetUsersRequest $request): array
{
$users = QueryBuilder::for(User::class)
$users = QueryBuilder::for(User::query())
->allowedFilters(['email', 'uuid', 'username', 'external_id'])
->allowedSorts(['id', 'uuid'])
->paginate($request->query('per_page') ?? 50);

View File

@@ -11,8 +11,6 @@ use App\Models\Filters\MultiFieldServerFilter;
use App\Transformers\Api\Client\ServerTransformer;
use App\Http\Requests\Api\Client\GetServersRequest;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
#[Group('Base')]
class ClientController extends ClientApiController
@@ -38,11 +36,10 @@ class ClientController extends ClientApiController
$user = $request->user();
$transformer = $this->getTransformer(ServerTransformer::class);
/** @var Builder<Model> $query */
$query = Server::query()->with($this->getIncludesForTransformer($transformer, ['node']));
// Start the query builder and ensure we eager load any requested relationships from the request.
$builder = QueryBuilder::for($query)->allowedFilters([
$builder = QueryBuilder::for(
Server::query()->with($this->getIncludesForTransformer($transformer, ['node']))
)->allowedFilters([
'uuid',
'name',
'description',

View File

@@ -19,7 +19,6 @@ use App\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use App\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest;
use App\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest;
use App\Http\Requests\Api\Client\Servers\Backups\RenameBackupRequest;
use Dedoc\Scramble\Attributes\Group;
#[Group('Server - Backup')]
@@ -196,35 +195,6 @@ class BackupController extends ClientApiController
]);
}
/**
* Rename backup
*
* Updates the name of a backup for a server instance.
*
* @return array<array-key, mixed>
*
* @throws \Throwable
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function rename(RenameBackupRequest $request, Server $server, Backup $backup): array
{
$oldName = $backup->name;
$newName = $request->input('name');
$backup->update(['name' => $newName]);
if ($oldName !== $newName) {
Activity::event('server:backup.rename')
->subject($backup)
->property(['old_name' => $oldName, 'new_name' => $newName])
->log();
}
return $this->fractal->item($backup)
->transformWith($this->getTransformer(BackupTransformer::class))
->toArray();
}
/**
* Restore backup
*

View File

@@ -138,7 +138,15 @@ class SubuserController extends ClientApiController
*/
protected function getDefaultPermissions(Request $request): array
{
$allowed = Permission::permissionKeys()->all();
$allowed = Permission::permissions()
->map(function ($value, $prefix) {
return array_map(function ($value) use ($prefix) {
return "$prefix.$value";
}, array_keys($value['keys']));
})
->flatten()
->all();
$cleaned = array_intersect($request->input('permissions') ?? [], $allowed);
return array_unique(array_merge($cleaned, [Permission::ACTION_WEBSOCKET_CONNECT]));

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers\Api\Remote;
use App\Models\Node;
use Carbon\Carbon;
use Illuminate\Support\Str;
use App\Models\User;
@@ -15,7 +14,7 @@ class ActivityProcessingController extends Controller
{
public function __invoke(ActivityEventRequest $request): void
{
/** @var Node $node */
/** @var \App\Models\Node $node */
$node = $request->attributes->get('node');
$servers = $node->servers()->whereIn('uuid', $request->servers())->get()->keyBy('uuid');
@@ -23,7 +22,7 @@ class ActivityProcessingController extends Controller
$logs = [];
foreach ($request->input('data') as $datum) {
/** @var Server|null $server */
/** @var \App\Models\Server|null $server */
$server = $servers->get($datum['server']);
if (is_null($server) || !Str::startsWith($datum['event'], 'server:')) {
continue;

View File

@@ -2,8 +2,7 @@
namespace App\Http\Controllers\Api\Remote\Servers;
use App\Enums\ContainerStatus;
use App\Http\Requests\Api\Remote\ServerRequest;
use Illuminate\Http\Request;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
@@ -13,11 +12,11 @@ class ServerContainersController extends Controller
/**
* Updates the server container's status on the Panel
*/
public function status(ServerRequest $request, Server $server): JsonResponse
public function status(Server $server, Request $request): JsonResponse
{
$status = ContainerStatus::tryFrom($request->json('data.new_state')) ?? ContainerStatus::Missing;
$status = fluent($request->json()->all())->get('data.new_state');
cache()->put("servers.$server->uuid.status", $status, now()->addHour());
cache()->put("servers.$server->uuid.container.status", $status, now()->addHour());
return new JsonResponse([]);
}

View File

@@ -3,10 +3,7 @@
namespace App\Http\Controllers\Api\Remote\Servers;
use App\Enums\ServerState;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Models\ActivityLog;
use App\Models\Backup;
use App\Models\Node;
use Illuminate\Http\Request;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
@@ -32,7 +29,7 @@ class ServerDetailsController extends Controller
* Returns details about the server that allows daemon to self-recover and ensure
* that the state of the server matches the Panel at all times.
*/
public function __invoke(ServerRequest $request, Server $server): JsonResponse
public function __invoke(Server $server): JsonResponse
{
return new JsonResponse([
'settings' => $this->configurationStructureService->handle($server),
@@ -45,7 +42,7 @@ class ServerDetailsController extends Controller
*/
public function list(Request $request): ServerConfigurationCollection
{
/** @var Node $node */
/** @var \App\Models\Node $node */
$node = $request->attributes->get('node');
// Avoid run-away N+1 SQL queries by preloading the relationships that are used
@@ -88,9 +85,9 @@ class ServerDetailsController extends Controller
->get();
$this->connection->transaction(function () use ($node, $servers) {
/** @var Server $server */
/** @var \App\Models\Server $server */
foreach ($servers as $server) {
/** @var ActivityLog|null $activity */
/** @var \App\Models\ActivityLog|null $activity */
$activity = $server->activity->first();
if (!$activity) {
continue;

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Api\Remote\Servers;
use App\Enums\ServerState;
use App\Http\Requests\Api\Remote\ServerRequest;
use Illuminate\Http\Response;
use App\Models\Server;
use Illuminate\Http\JsonResponse;
@@ -16,12 +15,14 @@ class ServerInstallController extends Controller
/**
* Returns installation information for a server.
*/
public function index(ServerRequest $request, Server $server): JsonResponse
public function index(Server $server): JsonResponse
{
$egg = $server->egg;
return new JsonResponse([
'container_image' => $server->egg->copy_script_container,
'entrypoint' => $server->egg->copy_script_entry,
'script' => $server->egg->copy_script_install,
'container_image' => $egg->copy_script_container,
'entrypoint' => $egg->copy_script_entry,
'script' => $egg->copy_script_install,
]);
}

View File

@@ -2,12 +2,12 @@
namespace App\Http\Controllers\Api\Remote\Servers;
use App\Http\Requests\Api\Remote\ServerRequest;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use App\Models\Allocation;
use App\Models\ServerTransfer;
use Illuminate\Database\ConnectionInterface;
use App\Http\Controllers\Controller;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
@@ -28,23 +28,14 @@ class ServerTransferController extends Controller
*
* @throws \Throwable
*/
public function failure(ServerRequest $request, Server $server): JsonResponse
public function failure(Server $server): JsonResponse
{
$transfer = $server->transfer;
if (is_null($transfer)) {
throw new ConflictHttpException('Server is not being transferred.');
}
$this->connection->transaction(function () use ($transfer) {
$transfer->forceFill(['successful' => false])->saveOrFail();
if ($transfer->new_allocation || $transfer->new_additional_allocations) {
$allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations);
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
}
});
return new JsonResponse([], Response::HTTP_NO_CONTENT);
return $this->processFailedTransfer($transfer);
}
/**
@@ -52,17 +43,16 @@ class ServerTransferController extends Controller
*
* @throws \Throwable
*/
public function success(ServerRequest $request, Server $server): JsonResponse
public function success(Server $server): JsonResponse
{
$transfer = $server->transfer;
if (is_null($transfer)) {
throw new ConflictHttpException('Server is not being transferred.');
}
/** @var Server $server */
$server = $this->connection->transaction(function () use ($server, $transfer) {
$data = [];
$data = [];
/** @var \App\Models\Server $server */
$server = $this->connection->transaction(function () use ($server, $transfer, $data) {
if ($transfer->old_allocation || $transfer->old_additional_allocations) {
$allocations = array_merge([$transfer->old_allocation], $transfer->old_additional_allocations);
// Remove the old allocations for the server and re-assign the server to the new
@@ -70,7 +60,6 @@ class ServerTransferController extends Controller
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
$data['allocation_id'] = $transfer->new_allocation;
}
$data['node_id'] = $transfer->new_node;
$server->update($data);
@@ -93,4 +82,24 @@ class ServerTransferController extends Controller
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
/**
* Release all the reserved allocations for this transfer and mark it as failed in
* the database.
*
* @throws \Throwable
*/
protected function processFailedTransfer(ServerTransfer $transfer): JsonResponse
{
$this->connection->transaction(function () use (&$transfer) {
$transfer->forceFill(['successful' => false])->saveOrFail();
if ($transfer->new_allocation || $transfer->new_additional_allocations) {
$allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations);
Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]);
}
});
return new JsonResponse([], Response::HTTP_NO_CONTENT);
}
}

View File

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

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use App\Extensions\Captcha\CaptchaService;
use Closure;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Events\Auth\FailedCaptcha;
use Symfony\Component\HttpKernel\Exception\HttpException;
readonly class VerifyCaptcha
{
public function __construct(private Application $app) {}
public function handle(Request $request, Closure $next, CaptchaService $captchaService): mixed
{
if ($this->app->isLocal()) {
return $next($request);
}
$schemas = $captchaService->getActiveSchemas();
foreach ($schemas as $schema) {
$response = $schema->validateResponse();
if ($response['success'] && $schema->verifyDomain($response['hostname'] ?? '', $request->url())) {
return $next($request);
}
event(new FailedCaptcha($request->ip(), $response['message'] ?? null));
throw new HttpException(Response::HTTP_BAD_REQUEST, "Failed to validate {$schema->getId()} captcha data.");
}
// No captcha enabled
return $next($request);
}
}

View File

@@ -55,7 +55,6 @@ class StoreServerRequest extends ApplicationApiRequest
// Automatic deployment rules
'deploy' => 'sometimes|required|array',
// Locations are deprecated, use tags
'deploy.locations' => 'sometimes|array',
'deploy.locations.*' => 'required_with:deploy.locations|integer|min:1',
'deploy.tags' => 'array',
@@ -177,6 +176,7 @@ class StoreServerRequest extends ApplicationApiRequest
$object->setDedicated($this->input('deploy.dedicated_ip', false));
$object->setTags($this->input('deploy.tags', $this->input('deploy.locations', [])));
$object->setPorts($this->input('deploy.port_range', []));
$object->setNode($this->input('deploy.node_id'));
return $object;
}

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Http\Requests\Api\Client\Servers\Backups;
use App\Models\Permission;
use App\Http\Requests\Api\Client\ClientApiRequest;
class RenameBackupRequest extends ClientApiRequest
{
public function permission(): string
{
return Permission::ACTION_BACKUP_DELETE;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
];
}
}

View File

@@ -22,8 +22,7 @@ class SendPowerRequest extends ClientApiRequest
return Permission::ACTION_CONTROL_RESTART;
}
// Fallback for invalid signals
return Permission::ACTION_WEBSOCKET_CONNECT;
return '__invalid';
}
/**

View File

@@ -2,8 +2,15 @@
namespace App\Http\Requests\Api\Remote;
class InstallationDataRequest extends ServerRequest
use Illuminate\Foundation\Http\FormRequest;
class InstallationDataRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, string|string[]>
*/

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Http\Requests\Api\Remote;
use App\Models\Node;
use App\Models\Server;
use Illuminate\Foundation\Http\FormRequest;
class ServerRequest extends FormRequest
{
public function authorize(): bool
{
/** @var Node $node */
$node = $this->attributes->get('node');
/** @var ?Server $server */
$server = $this->route()->parameter('server');
return $server && $server->node_id === $node->id;
}
}

View File

@@ -28,14 +28,14 @@ class ProcessWebhook implements ShouldQueue
public function handle(): void
{
$data = $this->data[0] ?? [];
if (count($data) === 1) {
$data = reset($data);
}
$data = is_array($data) ? $data : (json_decode($data, true) ?? []);
$data['event'] = $this->webhookConfiguration->transformClassName($this->eventName);
$data = $this->data[0];
if ($this->webhookConfiguration->type === WebhookType::Discord) {
$data = array_merge(
is_array($data) ? $data : json_decode($data, true),
['event' => $this->webhookConfiguration->transformClassName($this->eventName)]
);
$payload = json_encode($this->webhookConfiguration->payload);
$tmp = $this->webhookConfiguration->replaceVars($data, $payload);
$data = json_decode($tmp, true);
@@ -53,10 +53,9 @@ class ProcessWebhook implements ShouldQueue
}
try {
$customHeaders = $this->webhookConfiguration->headers;
$headers = [];
foreach ($customHeaders as $key => $value) {
$headers[$key] = $this->webhookConfiguration->replaceVars($data, $value);
if ($this->webhookConfiguration->type === WebhookType::Regular && $customHeaders = $this->webhookConfiguration->headers) {
$headers = array_merge(['X-Webhook-Event', $this->eventName], $customHeaders);
}
Http::withHeaders($headers)->post($this->webhookConfiguration->endpoint, $data)->throw();

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