mirror of
https://github.com/pelican-dev/panel.git
synced 2025-09-08 09:38:37 +02:00
Merge branch 'main' into vehikl/make-webhooks-normalized
This commit is contained in:
commit
dd2fdd15a1
14
Dockerfile
14
Dockerfile
@ -63,8 +63,8 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install additional required libraries
|
||||
RUN apk update && apk add --no-cache \
|
||||
caddy ca-certificates supervisor supercronic
|
||||
RUN apk add --no-cache \
|
||||
caddy ca-certificates supervisor supercronic fcgi
|
||||
|
||||
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
|
||||
@ -85,7 +85,8 @@ RUN chown root:www-data ./ \
|
||||
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/
|
||||
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
@ -93,10 +94,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||
# Add Laravel scheduler to crontab
|
||||
COPY docker/crontab /etc/supercronic/crontab
|
||||
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
COPY docker/healthcheck.sh /healthcheck.sh
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
CMD /bin/ash /healthcheck.sh
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
@ -104,5 +106,5 @@ VOLUME /pelican-data
|
||||
|
||||
USER www-data
|
||||
|
||||
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
|
||||
ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
||||
|
@ -67,8 +67,8 @@ FROM --platform=$TARGETOS/$TARGETARCH base AS final
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install additional required libraries
|
||||
RUN apk update && apk add --no-cache \
|
||||
caddy ca-certificates supervisor supercronic
|
||||
RUN apk add --no-cache \
|
||||
caddy ca-certificates supervisor supercronic fcgi coreutils
|
||||
|
||||
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
|
||||
@ -89,7 +89,8 @@ RUN chown root:www-data ./ \
|
||||
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
|
||||
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/
|
||||
|
||||
# Configure Supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
@ -97,10 +98,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
|
||||
# Add Laravel scheduler to crontab
|
||||
COPY docker/crontab /etc/supercronic/crontab
|
||||
|
||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
COPY docker/healthcheck.sh /healthcheck.sh
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
CMD /bin/ash /healthcheck.sh
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
@ -108,5 +110,5 @@ VOLUME /pelican-data
|
||||
|
||||
USER www-data
|
||||
|
||||
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
|
||||
ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
||||
|
@ -2,10 +2,13 @@
|
||||
|
||||
namespace App\Console\Commands\Egg;
|
||||
|
||||
use App\Enums\EggFormat;
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use JsonException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class CheckEggUpdatesCommand extends Command
|
||||
{
|
||||
@ -23,6 +26,9 @@ class CheckEggUpdatesCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
private function check(Egg $egg, EggExporterService $exporterService): void
|
||||
{
|
||||
if (is_null($egg->update_url)) {
|
||||
@ -31,22 +37,26 @@ class CheckEggUpdatesCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$currentJson = json_decode($exporterService->handle($egg->id));
|
||||
unset($currentJson->exported_at);
|
||||
$ext = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
$isYaml = in_array($ext, ['yaml', 'yml']);
|
||||
|
||||
$updatedEgg = file_get_contents($egg->update_url);
|
||||
assert($updatedEgg !== false);
|
||||
$updatedJson = json_decode($updatedEgg);
|
||||
unset($updatedJson->exported_at);
|
||||
$local = $isYaml
|
||||
? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML))
|
||||
: json_decode($exporterService->handle($egg->id, EggFormat::JSON), true);
|
||||
|
||||
if (md5(json_encode($currentJson, JSON_THROW_ON_ERROR)) === md5(json_encode($updatedJson, JSON_THROW_ON_ERROR))) {
|
||||
$this->info("$egg->name: Up-to-date");
|
||||
cache()->put("eggs.$egg->uuid.update", false, now()->addHour());
|
||||
$remote = file_get_contents($egg->update_url);
|
||||
assert($remote !== false);
|
||||
|
||||
return;
|
||||
}
|
||||
$remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
|
||||
|
||||
$this->warn("$egg->name: Found update");
|
||||
cache()->put("eggs.$egg->uuid.update", true, now()->addHour());
|
||||
unset($local['exported_at'], $remote['exported_at']);
|
||||
|
||||
$localHash = md5(json_encode($local, JSON_THROW_ON_ERROR));
|
||||
$remoteHash = md5(json_encode($remote, JSON_THROW_ON_ERROR));
|
||||
|
||||
$status = $localHash === $remoteHash ? 'Up-to-date' : 'Found update';
|
||||
$this->{($localHash === $remoteHash) ? 'info' : 'warn'}("$egg->name: $status");
|
||||
|
||||
cache()->put("eggs.$egg->uuid.update", $localHash !== $remoteHash, now()->addHour());
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,6 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return str($this->value)->headline();
|
||||
return trans('server/backup.backup_status.' . strtolower($this->value));
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return str($this->value)->title();
|
||||
return trans('server/console.status.' . strtolower($this->value));
|
||||
}
|
||||
|
||||
public function isOffline(): bool
|
||||
|
9
app/Enums/EggFormat.php
Normal file
9
app/Enums/EggFormat.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum EggFormat: string
|
||||
{
|
||||
case YAML = 'yaml';
|
||||
case JSON = 'json';
|
||||
}
|
@ -2,9 +2,50 @@
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ServerResourceType
|
||||
use App\Models\Server;
|
||||
|
||||
enum ServerResourceType: string
|
||||
{
|
||||
case Unit;
|
||||
case Percentage;
|
||||
case Time;
|
||||
case Uptime = 'uptime';
|
||||
case CPU = 'cpu_absolute';
|
||||
case Memory = 'memory_bytes';
|
||||
case Disk = 'disk_bytes';
|
||||
|
||||
case CPULimit = 'cpu';
|
||||
case MemoryLimit = 'memory';
|
||||
case DiskLimit = 'disk';
|
||||
|
||||
/**
|
||||
* @return int resource amount in bytes
|
||||
*/
|
||||
public function getResourceAmount(Server $server): int
|
||||
{
|
||||
if ($this->isLimit()) {
|
||||
$resourceAmount = $server->{$this->value} ?? 0;
|
||||
|
||||
if (!$this->isPercentage()) {
|
||||
// Our limits are entered as MiB/ MB so we need to convert them to bytes
|
||||
$resourceAmount *= config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000;
|
||||
}
|
||||
|
||||
return $resourceAmount;
|
||||
}
|
||||
|
||||
return $server->retrieveResources()[$this->value] ?? 0;
|
||||
}
|
||||
|
||||
public function isLimit(): bool
|
||||
{
|
||||
return $this === ServerResourceType::CPULimit || $this === ServerResourceType::MemoryLimit || $this === ServerResourceType::DiskLimit;
|
||||
}
|
||||
|
||||
public function isTime(): bool
|
||||
{
|
||||
return $this === ServerResourceType::Uptime;
|
||||
}
|
||||
|
||||
public function isPercentage(): bool
|
||||
{
|
||||
return $this === ServerResourceType::CPU || $this === ServerResourceType::CPULimit;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum ServerState: string implements HasColor, HasIcon, HasLabel
|
||||
{
|
||||
case Normal = 'normal';
|
||||
case Installing = 'installing';
|
||||
case InstallFailed = 'install_failed';
|
||||
case ReinstallFailed = 'reinstall_failed';
|
||||
@ -18,7 +17,6 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
|
||||
public function getIcon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Normal => 'tabler-heart',
|
||||
self::Installing => 'tabler-heart-bolt',
|
||||
self::InstallFailed => 'tabler-heart-x',
|
||||
self::ReinstallFailed => 'tabler-heart-x',
|
||||
@ -31,14 +29,13 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
|
||||
{
|
||||
if ($hex) {
|
||||
return match ($this) {
|
||||
self::Normal, self::Installing, self::RestoringBackup => '#2563EB',
|
||||
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',
|
||||
|
@ -32,4 +32,8 @@ interface OAuthSchemaInterface
|
||||
public function getHexColor(): ?string;
|
||||
|
||||
public function isEnabled(): bool;
|
||||
|
||||
public function shouldCreateMissingUsers(): bool;
|
||||
|
||||
public function shouldLinkMissingUsers(): bool;
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ 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
|
||||
@ -53,6 +55,28 @@ 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")),
|
||||
];
|
||||
}
|
||||
|
||||
@ -96,4 +120,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
@ -169,16 +169,6 @@ class Settings extends Page implements HasForms
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
|
||||
->default(env('APP_DEBUG', config('app.debug'))),
|
||||
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
|
||||
->label(trans('admin/setting.general.navigation'))
|
||||
->inline()
|
||||
->options([
|
||||
false => trans('admin/setting.general.sidebar'),
|
||||
true => trans('admin/setting.general.topbar'),
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
|
||||
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
|
||||
]),
|
||||
Group::make()
|
||||
->columns(2)
|
||||
|
@ -29,7 +29,7 @@ class EggResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
|
||||
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
|
@ -207,7 +207,7 @@ class CreateEgg extends CreateRecord
|
||||
'*' => trans('admin/egg.error_reserved'),
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255),
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
|
||||
Fieldset::make(trans('admin/egg.user_permissions'))
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
|
||||
|
@ -196,7 +196,7 @@ class EditEgg extends EditRecord
|
||||
'*' => trans('admin/egg.error_reserved'),
|
||||
])
|
||||
->required(),
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255),
|
||||
TextInput::make('default_value')->label(trans('admin/egg.default_value')),
|
||||
Fieldset::make(trans('admin/egg.user_permissions'))
|
||||
->schema([
|
||||
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
|
||||
|
@ -40,7 +40,8 @@ class NodeResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
|
||||
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
|
||||
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
|
@ -59,7 +59,7 @@ class RoleResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
|
||||
return !empty(auth()->user()->getCustomization()['top_navigation']) ? trans('admin/dashboard.advanced') : trans('admin/dashboard.user');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
|
@ -43,7 +43,7 @@ class ServerResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server');
|
||||
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.server');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
|
@ -56,7 +56,7 @@ class UserResource extends Resource
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.user');
|
||||
return !empty(auth()->user()->getCustomization()['top_navigation']) ? false : trans('admin/dashboard.user');
|
||||
}
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
|
@ -19,6 +19,7 @@ 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;
|
||||
@ -129,23 +130,12 @@ class WebhookResource extends Resource
|
||||
->live()
|
||||
->inline()
|
||||
->options(WebhookType::class)
|
||||
->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();
|
||||
}
|
||||
}),
|
||||
->default(WebhookType::Regular->value),
|
||||
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)),
|
||||
@ -153,6 +143,15 @@ 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)
|
||||
@ -163,8 +162,6 @@ 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()
|
||||
@ -183,7 +180,10 @@ class WebhookResource extends Resource
|
||||
{
|
||||
return [
|
||||
KeyValue::make('headers')
|
||||
->label(trans('admin/webhook.headers')),
|
||||
->label(trans('admin/webhook.headers'))
|
||||
->default(fn () => [
|
||||
'X-Webhook-Event' => '{{event}}',
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -63,4 +63,15 @@ class CreateWebhookConfiguration extends CreateRecord
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return EditWebhookConfiguration::getUrl(['record' => $this->getRecord()]);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
parent::mount();
|
||||
WebhookResource::sendHelpBanner();
|
||||
}
|
||||
}
|
||||
|
@ -123,4 +123,10 @@ class EditWebhookConfiguration extends EditRecord
|
||||
{
|
||||
$this->dispatch('refresh-widget');
|
||||
}
|
||||
|
||||
public function mount(int|string $record): void
|
||||
{
|
||||
parent::mount($record);
|
||||
WebhookResource::sendHelpBanner();
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ class ListServers extends ListRecords
|
||||
TextColumn::make('condition')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->tooltip(fn (Server $server) => $server->formatResource('uptime', type: ServerResourceType::Time))
|
||||
->tooltip(fn (Server $server) => $server->formatResource(ServerResourceType::Uptime))
|
||||
->icon(fn (Server $server) => $server->condition->getIcon())
|
||||
->color(fn (Server $server) => $server->condition->getColor()),
|
||||
TextColumn::make('name')
|
||||
@ -78,22 +78,22 @@ class ListServers extends ListRecords
|
||||
->copyable(request()->isSecure())
|
||||
->state(fn (Server $server) => $server->allocation->address ?? 'None'),
|
||||
TextColumn::make('cpuUsage')
|
||||
->label('Resources')
|
||||
->label(trans('server/dashboard.resources'))
|
||||
->icon('tabler-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))
|
||||
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::CPULimit)]))
|
||||
->state(fn (Server $server) => $server->formatResource(ServerResourceType::CPU))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'cpu')),
|
||||
TextColumn::make('memoryUsage')
|
||||
->label('')
|
||||
->icon('tabler-device-desktop-analytics')
|
||||
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('memory', limit: true))
|
||||
->state(fn (Server $server) => $server->formatResource('memory_bytes'))
|
||||
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::MemoryLimit)]))
|
||||
->state(fn (Server $server) => $server->formatResource(ServerResourceType::Memory))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'memory')),
|
||||
TextColumn::make('diskUsage')
|
||||
->label('')
|
||||
->icon('tabler-device-sd-card')
|
||||
->tooltip(fn (Server $server) => 'Usage Limit: ' . $server->formatResource('disk', limit: true))
|
||||
->state(fn (Server $server) => $server->formatResource('disk_bytes'))
|
||||
->tooltip(fn (Server $server) => trans('server/dashboard.usage_limit', ['resource' => $server->formatResource(ServerResourceType::DiskLimit)]))
|
||||
->state(fn (Server $server) => $server->formatResource(ServerResourceType::Disk))
|
||||
->color(fn (Server $server) => $this->getResourceColor($server, 'disk')),
|
||||
];
|
||||
}
|
||||
|
@ -2,9 +2,12 @@
|
||||
|
||||
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
|
||||
{
|
||||
@ -21,8 +24,30 @@ class ExportEggAction extends Action
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can('export egg'));
|
||||
|
||||
$this->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'));
|
||||
$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') . ' .json')
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::JSON);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->close(),
|
||||
Action::make('yaml')
|
||||
->label(trans('admin/egg.export.as') . ' .yaml')
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::YAML);
|
||||
}, 'egg-' . $egg->getKebabName() . '.yaml'))
|
||||
->close(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -47,13 +47,31 @@ class ImportEggAction extends Action
|
||||
|
||||
foreach ($eggs as $egg) {
|
||||
if ($egg instanceof TemporaryUploadedFile) {
|
||||
$name = str($egg->getClientOriginalName())->afterLast('egg-')->before('.json')->headline();
|
||||
$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(),
|
||||
};
|
||||
$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);
|
||||
@ -94,7 +112,7 @@ class ImportEggAction extends Action
|
||||
FileUpload::make('files')
|
||||
->label(trans('admin/egg.model_label'))
|
||||
->hint(trans('admin/egg.import.egg_help'))
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->acceptedFileTypes(['application/json', 'application/yaml', 'application/x-yaml', 'text/yaml'])
|
||||
->preserveFilenames()
|
||||
->previewable(false)
|
||||
->storeFiles(false)
|
||||
@ -125,7 +143,7 @@ class ImportEggAction extends Action
|
||||
}),
|
||||
Repeater::make('urls')
|
||||
->label('')
|
||||
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->before('.json')->headline())
|
||||
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->beforeLast('.')->headline())
|
||||
->hint(trans('admin/egg.import.url_help'))
|
||||
->addActionLabel(trans('admin/egg.import.add_url'))
|
||||
->grid($isMultiple ? 2 : null)
|
||||
@ -139,7 +157,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')
|
||||
->endsWith(['.json', '.yaml', '.yml'])
|
||||
->validationAttribute(trans('admin/egg.import.url')),
|
||||
]),
|
||||
]),
|
||||
|
@ -39,26 +39,26 @@ class ImportScheduleAction extends Action
|
||||
Tabs::make('Tabs')
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/schedule.import.file'))
|
||||
Tab::make(trans('server/schedule.import_action.file'))
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
->label(trans('admin/schedule.model_label'))
|
||||
->hint(trans('admin/schedule.import.schedule_help'))
|
||||
->hiddenLabel()
|
||||
->hint(trans('server/schedule.import_action.schedule_help'))
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->preserveFilenames()
|
||||
->previewable(false)
|
||||
->storeFiles(false)
|
||||
->multiple(true),
|
||||
]),
|
||||
Tab::make(trans('admin/schedule.import.url'))
|
||||
Tab::make(trans('server/schedule.import_action.url'))
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
Repeater::make('urls')
|
||||
->label('')
|
||||
->hiddenLabel()
|
||||
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/schedule-')->before('.json')->headline())
|
||||
->hint(trans('admin/schedule.import.url_help'))
|
||||
->addActionLabel(trans('admin/schedule.import.add_url'))
|
||||
->hint(trans('server/schedule.import_action.url_help'))
|
||||
->addActionLabel(trans('server/schedule.import_action.add_url'))
|
||||
->grid(2)
|
||||
->reorderable(false)
|
||||
->addable(true)
|
||||
@ -66,10 +66,10 @@ class ImportScheduleAction extends Action
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->live()
|
||||
->label(trans('admin/schedule.import.url'))
|
||||
->label(trans('server/schedule.import_action.url'))
|
||||
->url()
|
||||
->endsWith('.json')
|
||||
->validationAttribute(trans('admin/schedule.import.url')),
|
||||
->validationAttribute(trans('server/schedule.import_action.url')),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
@ -104,14 +104,14 @@ class ImportScheduleAction extends Action
|
||||
|
||||
if ($failed->count() > 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/schedule.import.import_failed'))
|
||||
->title(trans('server/schedule.import_action.import_failed'))
|
||||
->body($failed->join(', '))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
if ($success->count() > 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/schedule.import.import_success'))
|
||||
->title(trans('server/schedule.import_action.import_success'))
|
||||
->body($success->join(', '))
|
||||
->success()
|
||||
->send();
|
||||
|
@ -15,6 +15,11 @@ class PreviewStartupAction extends Action
|
||||
return 'preview';
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return trans('server/startup.preview');
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
@ -2,8 +2,11 @@
|
||||
|
||||
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
|
||||
@ -23,8 +26,30 @@ class ExportEggAction extends Action
|
||||
|
||||
$this->authorize(fn () => auth()->user()->can('export egg'));
|
||||
|
||||
$this->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'));
|
||||
$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') . ' .json')
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::JSON);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->close(),
|
||||
Action::make('yaml')
|
||||
->label(trans('admin/egg.export.as') . ' .yaml')
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::YAML);
|
||||
}, 'egg-' . $egg->getKebabName() . '.yaml'))
|
||||
->close(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ class ImportEggAction extends Action
|
||||
}
|
||||
}),
|
||||
Repeater::make('urls')
|
||||
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->before('.json')->headline())
|
||||
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->beforeLast('.')->headline())
|
||||
->hint(trans('admin/egg.import.url_help'))
|
||||
->addActionLabel(trans('admin/egg.import.add_url'))
|
||||
->grid($isMultiple ? 2 : null)
|
||||
@ -131,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')
|
||||
->endsWith(['.json', '.yaml', '.yml'])
|
||||
->validationAttribute(trans('admin/egg.import.url')),
|
||||
]),
|
||||
]),
|
||||
|
@ -174,7 +174,7 @@ class EditProfile extends BaseEditProfile
|
||||
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name)
|
||||
->label(trans('profile.' . ($unlink ? 'unlink' : 'link'), ['name' => $name]))
|
||||
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
|
||||
->color(Color::hex($schema->getHexColor()))
|
||||
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
|
||||
@ -322,6 +322,7 @@ class EditProfile extends BaseEditProfile
|
||||
Section::make(trans('profile.api_keys'))->columnSpan(2)->schema([
|
||||
Repeater::make('api_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
@ -406,6 +407,7 @@ class EditProfile extends BaseEditProfile
|
||||
Section::make(trans('profile.ssh_keys'))->columnSpan(2)->schema([
|
||||
Repeater::make('ssh_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('sshKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['name'])
|
||||
@ -445,14 +447,17 @@ class EditProfile extends BaseEditProfile
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->label('')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
})
|
||||
->schema([
|
||||
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
Placeholder::make('log')
|
||||
->hiddenLabel()
|
||||
->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
|
||||
@ -471,6 +476,14 @@ 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()
|
||||
@ -628,9 +641,10 @@ 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']);
|
||||
unset($data['console_font'],$data['console_font_size'], $data['console_rows'], $data['dashboard_layout'], $data['top_navigation']);
|
||||
$data['customization'] = json_encode($moarbetterdata);
|
||||
|
||||
return $data;
|
||||
@ -645,6 +659,7 @@ 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;
|
||||
}
|
||||
|
@ -162,6 +162,7 @@ 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])
|
||||
@ -169,6 +170,7 @@ 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])
|
||||
@ -176,6 +178,7 @@ 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])
|
||||
@ -184,8 +187,9 @@ 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('This can result in data corruption and/or data loss!')
|
||||
->tooltip(trans('server/console.power_actions.kill_tooltip'))
|
||||
->size(ActionSize::ExtraLarge)
|
||||
->dispatch('setServerState', ['state' => 'kill', 'uuid' => $server->uuid])
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_CONTROL_STOP, $server))
|
||||
@ -193,4 +197,14 @@ 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');
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class Settings extends ServerFormPage
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Section::make('Server Information')
|
||||
Section::make(trans('server/setting.server_info.title'))
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@ -47,11 +47,11 @@ class Settings extends ServerFormPage
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Fieldset::make('Server')
|
||||
->label('Information')
|
||||
Fieldset::make()
|
||||
->label(trans('server/setting.server_info.information'))
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Server Name')
|
||||
->label(trans('server/setting.server_info.name'))
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
|
||||
->required()
|
||||
->columnSpan([
|
||||
@ -63,7 +63,7 @@ class Settings extends ServerFormPage
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)),
|
||||
Textarea::make('description')
|
||||
->label('Server Description')
|
||||
->label(trans('server/setting.server_info.description'))
|
||||
->hidden(!config('panel.editable_server_descriptions'))
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
|
||||
->columnSpan([
|
||||
@ -76,7 +76,7 @@ class Settings extends ServerFormPage
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
|
||||
TextInput::make('uuid')
|
||||
->label('Server UUID')
|
||||
->label(trans('server/setting.server_info.uuid'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
@ -85,12 +85,12 @@ class Settings extends ServerFormPage
|
||||
])
|
||||
->disabled(),
|
||||
TextInput::make('id')
|
||||
->label('Server ID')
|
||||
->label(trans('server/setting.server_info.id'))
|
||||
->disabled()
|
||||
->columnSpan(1),
|
||||
]),
|
||||
Fieldset::make('Limits')
|
||||
->label('Limits')
|
||||
Fieldset::make()
|
||||
->label(trans('server/setting.server_info.limits.title'))
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
@ -100,57 +100,56 @@ class Settings extends ServerFormPage
|
||||
->schema([
|
||||
TextInput::make('cpu')
|
||||
->label('')
|
||||
->prefix('CPU')
|
||||
->prefix(trans('server/setting.server_info.limits.cpu'))
|
||||
->prefixIcon('tabler-cpu')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : Number::format($server->cpu, locale: auth()->user()->language) . '%'),
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : Number::format($server->cpu, locale: auth()->user()->language) . '%'),
|
||||
TextInput::make('memory')
|
||||
->label('')
|
||||
->prefix('Memory')
|
||||
->prefix(trans('server/setting.server_info.limits.memory'))
|
||||
->prefixIcon('tabler-device-desktop-analytics')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : convert_bytes_to_readable($server->memory * 2 ** 20)),
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : convert_bytes_to_readable($server->memory * 2 ** 20)),
|
||||
TextInput::make('disk')
|
||||
->label('')
|
||||
->prefix('Disk Space')
|
||||
->prefix(trans('server/setting.server_info.limits.disk'))
|
||||
->prefixIcon('tabler-device-sd-card')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'Unlimited' : convert_bytes_to_readable($server->disk * 2 ** 20)),
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.unlimited') : convert_bytes_to_readable($server->disk * 2 ** 20)),
|
||||
TextInput::make('backup_limit')
|
||||
->label('')
|
||||
->prefix('Backups')
|
||||
->prefix(trans('server/setting.server_info.limits.backups'))
|
||||
->prefixIcon('tabler-file-zip')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups' : $server->backups->count() . ' of ' . $state),
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups' : $server->backups->count() . ' ' .trans('server/setting.server_info.limits.of') . ' ' . $state),
|
||||
TextInput::make('database_limit')
|
||||
->label('')
|
||||
->prefix('Databases')
|
||||
->prefix(trans('server/setting.server_info.limits.databases'))
|
||||
->prefixIcon('tabler-database')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases' : $server->databases->count() . ' of ' . $state),
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases' : $server->databases->count() . ' ' . trans('server/setting.server_info.limits.of') . ' ' .$state),
|
||||
TextInput::make('allocation_limit')
|
||||
->label('')
|
||||
->prefix('Allocations')
|
||||
->prefix(trans('server/setting.server_info.limits.allocations'))
|
||||
->prefixIcon('tabler-network')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Additional Allocations' : $server->allocations->count() . ' of ' . $state),
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? trans('server/setting.server_info.limits.no_allocations') : $server->allocations->count() . ' ' .trans('server/setting.server_info.limits.of') . ' ' . $state),
|
||||
]),
|
||||
]),
|
||||
Section::make('Node Information')
|
||||
Section::make(trans('server/setting.node_info.title'))
|
||||
->schema([
|
||||
TextInput::make('node.name')
|
||||
->label('Node Name')
|
||||
->label(trans('server/setting.node_info.name'))
|
||||
->formatStateUsing(fn (Server $server) => $server->node->name)
|
||||
->disabled(),
|
||||
Fieldset::make('SFTP Information')
|
||||
Fieldset::make(trans('server/setting.node_info.sftp.title'))
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_FILE_SFTP, $server))
|
||||
->label('SFTP Information')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
@ -159,13 +158,13 @@ class Settings extends ServerFormPage
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('connection')
|
||||
->label('Connection')
|
||||
->label(trans('server/setting.node_info.sftp.connection'))
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->suffixAction(fn () => request()->isSecure() ? CopyAction::make() : null)
|
||||
->hintAction(
|
||||
Action::make('connect_sftp')
|
||||
->label('Connect to SFTP')
|
||||
->label(trans('server/setting.node_info.sftp.action'))
|
||||
->color('success')
|
||||
->icon('tabler-plug')
|
||||
->url(function (Server $server) {
|
||||
@ -180,28 +179,29 @@ class Settings extends ServerFormPage
|
||||
return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
}),
|
||||
TextInput::make('username')
|
||||
->label('Username')
|
||||
->label(trans('server/setting.node_info.sftp.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('Your SFTP password is the same as the password you use to access this panel.'),
|
||||
->content(trans('server/setting.node_info.sftp.password_body')),
|
||||
]),
|
||||
]),
|
||||
Section::make('Reinstall Server')
|
||||
Section::make(trans('server/setting.reinstall.title'))
|
||||
->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('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')
|
||||
->modalHeading(trans('server/setting.reinstall.modal'))
|
||||
->modalDescription(trans('server/setting.reinstall.modal_description'))
|
||||
->modalSubmitActionLabel(trans('server/setting.reinstall.yes'))
|
||||
->action(function (Server $server, ReinstallServerService $reinstallService) {
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403);
|
||||
|
||||
@ -211,9 +211,9 @@ class Settings extends ServerFormPage
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Server Reinstall failed')
|
||||
->title(trans('server/setting.reinstall.notification_fail'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
@ -223,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());
|
||||
@ -233,9 +233,9 @@ class Settings extends ServerFormPage
|
||||
->footerActionsAlignment(Alignment::Right)
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->label('Reinstalling your server will stop it, and then re-run the installation script that initially set it up.'),
|
||||
->label(trans('server/setting.reinstall.body')),
|
||||
Placeholder::make('')
|
||||
->label('Some files may be deleted or modified during this process, please back up your data before continuing.'),
|
||||
->label(trans('server/setting.reinstall.body2')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@ -258,15 +258,15 @@ class Settings extends ServerFormPage
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Updated Server Name')
|
||||
->title(trans('server/setting.notification_name'))
|
||||
->body(fn () => $original . ' -> ' . $name)
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed')
|
||||
->title(trans('server/setting.failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
@ -289,16 +289,26 @@ class Settings extends ServerFormPage
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Updated Server Description')
|
||||
->title(trans('server/setting.notification_description'))
|
||||
->body(fn () => $original . ' -> ' . $description)
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed')
|
||||
->title(trans('server/setting.failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/setting.title');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/setting.title');
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ 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;
|
||||
|
||||
@ -43,7 +44,7 @@ class Startup extends ServerFormPage
|
||||
Hidden::make('previewing')
|
||||
->default(false),
|
||||
Textarea::make('startup')
|
||||
->label('Startup Command')
|
||||
->label(trans('server/startup.command'))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
@ -51,10 +52,10 @@ class Startup extends ServerFormPage
|
||||
'lg' => 4,
|
||||
])
|
||||
->autosize()
|
||||
->hintAction(PreviewStartupAction::make('preview'))
|
||||
->hintAction(PreviewStartupAction::make())
|
||||
->readOnly(),
|
||||
TextInput::make('custom_image')
|
||||
->label('Docker Image')
|
||||
->label(trans('server/startup.docker_image'))
|
||||
->readOnly()
|
||||
->visible(fn (Server $server) => !in_array($server->image, $server->egg->docker_images))
|
||||
->formatStateUsing(fn (Server $server) => $server->image)
|
||||
@ -65,7 +66,7 @@ class Startup extends ServerFormPage
|
||||
'lg' => 2,
|
||||
]),
|
||||
Select::make('image')
|
||||
->label('Docker Image')
|
||||
->label(trans('server/startup.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))
|
||||
@ -80,8 +81,8 @@ class Startup extends ServerFormPage
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Docker image updated')
|
||||
->body('Restart the server to use the new image.')
|
||||
->title(trans('server/startup.notification_docker'))
|
||||
->body(trans('server/startup.notification_docker_body'))
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
@ -97,10 +98,10 @@ class Startup extends ServerFormPage
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Section::make('Server Variables')
|
||||
Section::make(trans('server/startup.variables'))
|
||||
->schema([
|
||||
Repeater::make('server_variables')
|
||||
->label('')
|
||||
->hiddenLabel()
|
||||
->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))
|
||||
@ -207,9 +208,9 @@ class Startup extends ServerFormPage
|
||||
|
||||
if ($validator->fails()) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Validation Failed: ' . $serverVariable->variable->name)
|
||||
->title(trans('server/startup.validation_fail', ['variable' => $serverVariable->variable->name]))
|
||||
->body(implode(', ', $validator->errors()->all()))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return null;
|
||||
@ -232,18 +233,28 @@ class Startup extends ServerFormPage
|
||||
->log();
|
||||
}
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Updated: ' . $serverVariable->variable->name)
|
||||
->title(trans('server/startup.update', ['variable' => $serverVariable->variable->name]))
|
||||
->body(fn () => $original . ' -> ' . $state)
|
||||
->success()
|
||||
->send();
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed: ' . $serverVariable->variable->name)
|
||||
->title(trans('server/startup.fail', ['variable' => $serverVariable->variable->name]))
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return trans('server/startup.title');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/startup.title');
|
||||
}
|
||||
}
|
||||
|
@ -38,10 +38,6 @@ 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';
|
||||
@ -56,14 +52,16 @@ 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 ? 'System' : 'Deleted user';
|
||||
return $activityLog->actor_id === null ? trans('server/activity.system') : trans('server/activity.deleted_user');
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
@ -79,6 +77,7 @@ 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),
|
||||
@ -89,11 +88,13 @@ 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 ? 'System' : 'Deleted user';
|
||||
return $activityLog->actor_id === null ? trans('server/activity.system') : trans('server/activity.deleted_user');
|
||||
}
|
||||
|
||||
$user = $activityLog->actor->username;
|
||||
@ -116,9 +117,10 @@ 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'),
|
||||
DateTimePicker::make('timestamp')
|
||||
->label(trans('server/activity.timestamp')),
|
||||
KeyValue::make('properties')
|
||||
->label('Metadata')
|
||||
->label(trans('server/activity.metadata'))
|
||||
->formatStateUsing(fn ($state) => Arr::dot($state)),
|
||||
]),
|
||||
])
|
||||
@ -168,4 +170,9 @@ class ActivityResource extends Resource
|
||||
'index' => Pages\ListActivities::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/activity.title');
|
||||
}
|
||||
}
|
||||
|
@ -18,4 +18,9 @@ class ListActivities extends ListRecords
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/activity.title');
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,6 @@ 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';
|
||||
@ -46,16 +42,17 @@ class AllocationResource extends Resource
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('ip')
|
||||
->label('Address')
|
||||
->label(trans('server/network.address'))
|
||||
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
|
||||
TextColumn::make('alias')
|
||||
->hidden(),
|
||||
TextColumn::make('port'),
|
||||
TextColumn::make('port')
|
||||
->label(trans('server/network.port')),
|
||||
TextInputColumn::make('notes')
|
||||
->label(trans('server/network.notes'))
|
||||
->visibleFrom('sm')
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
|
||||
->label('Notes')
|
||||
->placeholder('No Notes'),
|
||||
->placeholder(trans('server/network.no_notes')),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
@ -65,15 +62,15 @@ class AllocationResource extends Resource
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->tooltip(fn (Allocation $allocation) => ($allocation->id === $server->allocation_id ? 'Already' : 'Make') . ' Primary')
|
||||
->tooltip(fn (Allocation $allocation) => $allocation->id === $server->allocation_id ? trans('server/network.primary') : trans('server/network.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('Primary'),
|
||||
->label(trans('server/network.primary')),
|
||||
])
|
||||
->actions([
|
||||
DetachAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
|
||||
->label('Delete')
|
||||
->label(trans('server/network.delete'))
|
||||
->icon('tabler-trash')
|
||||
->action(function (Allocation $allocation) {
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
@ -117,4 +114,9 @@ class AllocationResource extends Resource
|
||||
'index' => Pages\ListAllocations::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/network.title');
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class ListAllocations extends ListRecords
|
||||
->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 ? 'Allocation limit reached' : 'Add Allocation')
|
||||
->tooltip(fn () => $server->allocations()->count() >= $server->allocation_limit ? trans('server/network.limit') : trans('server/network.add'))
|
||||
->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,4 +56,14 @@ class ListAllocations extends ListRecords
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/network.title');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/network.title');
|
||||
}
|
||||
}
|
||||
|
@ -79,14 +79,15 @@ class BackupResource extends Resource
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->label(trans('server/backup.actions.create.name'))
|
||||
->columnSpanFull(),
|
||||
TextArea::make('ignored')
|
||||
->columnSpanFull()
|
||||
->label('Ignored Files & Directories'),
|
||||
->label(trans('server/backup.actions.create.ignored'))
|
||||
->columnSpanFull(),
|
||||
Toggle::make('is_locked')
|
||||
->label('Lock?')
|
||||
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
|
||||
->label(trans('server/backup.actions.create.locked'))
|
||||
->helperText(trans('server/backup.actions.create.lock_helper'))
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -98,60 +99,94 @@ class BackupResource extends Resource
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(trans('server/backup.actions.create.name'))
|
||||
->searchable(),
|
||||
BytesColumn::make('bytes')
|
||||
->label('Size'),
|
||||
->label(trans('server/backup.size')),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label('Created')
|
||||
->label(trans('server/backup.created_at'))
|
||||
->since()
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->label(trans('server/backup.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('Rename')
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Backup 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('Backup Renamed')
|
||||
->body('The backup has been successfully renamed.')
|
||||
->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 ? 'Lock' : 'Unlock')
|
||||
->label(fn (Backup $backup) => !$backup->is_locked ? trans('server/backup.actions.lock.lock') : trans('server/backup.actions.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('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.'),
|
||||
->helperText(trans('server/backup.actions.restore.helper')),
|
||||
Checkbox::make('truncate')
|
||||
->label('Delete all files before restoring backup?'),
|
||||
->label(trans('server/backup.actions.restore.delete_all')),
|
||||
])
|
||||
->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();
|
||||
}
|
||||
|
||||
@ -174,21 +209,26 @@ class BackupResource extends Resource
|
||||
});
|
||||
|
||||
return Notification::make()
|
||||
->title('Restoring Backup')
|
||||
->title(trans('server/backup.actions.restore.notification_started'))
|
||||
->send();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
DeleteAction::make('delete')
|
||||
->disabled(fn (Backup $backup) => $backup->is_locked)
|
||||
->modalDescription(fn (Backup $backup) => 'Do you wish to delete ' . $backup->name . '?')
|
||||
->modalSubmitActionLabel('Delete Backup')
|
||||
->modalDescription(fn (Backup $backup) => trans('server/backup.actions.delete.description', ['backup' => $backup->name]))
|
||||
->modalSubmitActionLabel(trans('server/backup.actions.delete.title'))
|
||||
->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('Could not delete backup')
|
||||
->body('Connection to node failed')
|
||||
->title(trans('server/backup.actions.delete.notification_fail'))
|
||||
->body(trans('server/backup.actions.delete.notification_fail_body'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@ -227,4 +267,9 @@ class BackupResource extends Resource
|
||||
'index' => Pages\ListBackups::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/backup.title');
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class ListBackups extends ListRecords
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
|
||||
->icon('tabler-file-zip')->iconButton()->iconSize(IconSize::Large)
|
||||
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
|
||||
->tooltip(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup Limit Reached' : 'Create Backup') // Disabled Buttons have no tooltips in v3 :/
|
||||
->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 +55,15 @@ class ListBackups extends ListRecords
|
||||
->log();
|
||||
|
||||
return Notification::make()
|
||||
->title('Backup Created')
|
||||
->body($backup->name . ' created.')
|
||||
->title(trans('server/backup.actions.create.notification_success'))
|
||||
->body(trans('server/backup.actions.create.created', ['name' => $backup->name]))
|
||||
->success()
|
||||
->send();
|
||||
} catch (HttpException $e) {
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Failed')
|
||||
->title(trans('server/backup.actions.create.notification_fail'))
|
||||
->body($e->getMessage() . ' Try again' . ($e->getHeaders()['Retry-After'] ? ' in ' . $e->getHeaders()['Retry-After'] . ' seconds.' : ''))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
@ -74,4 +74,9 @@ class ListBackups extends ListRecords
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/backup.title');
|
||||
}
|
||||
}
|
||||
|
@ -66,13 +66,17 @@ 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(
|
||||
@ -82,11 +86,12 @@ 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('Connections From'),
|
||||
->label(trans('server/database.remote')),
|
||||
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('JDBC Connection String')
|
||||
->label(trans('server/database.jdbc'))
|
||||
->password()->revealable()
|
||||
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
|
||||
@ -100,12 +105,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'),
|
||||
TextColumn::make('username'),
|
||||
TextColumn::make('remote'),
|
||||
TextColumn::make('database')
|
||||
->label(trans('server/database.database')),
|
||||
TextColumn::make('username')
|
||||
->label(trans('server/database.username')),
|
||||
TextColumn::make('remote')
|
||||
->label(trans('server/database.remote')),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label(trans('server/database.created_at'))
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
@ -148,4 +158,9 @@ class DatabaseResource extends Resource
|
||||
'index' => Pages\ListDatabases::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/database.title');
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class ListDatabases extends ListRecords
|
||||
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 ? 'Database limit reached' : 'Create Database')
|
||||
->tooltip(fn () => $server->databases()->count() >= $server->database_limit ? trans('server/database.limit') : trans('server/database.create_database'))
|
||||
->disabled(fn () => $server->databases()->count() >= $server->database_limit)
|
||||
->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary')
|
||||
->createAnother(false)
|
||||
@ -44,20 +44,20 @@ class ListDatabases extends ListRecords
|
||||
->columns(2)
|
||||
->schema([
|
||||
Select::make('database_host_id')
|
||||
->label('Database Host')
|
||||
->label(trans('server/database.database_host'))
|
||||
->columnSpan(2)
|
||||
->required()
|
||||
->placeholder('Select Database Host')
|
||||
->placeholder(trans('server/database.database_host_select'))
|
||||
->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('Leaving this blank will auto generate a random name'),
|
||||
->hintIconTooltip(trans('server/database.name_hint')),
|
||||
TextInput::make('remote')
|
||||
->label(trans('server/database.connections_from'))
|
||||
->columnSpan(1)
|
||||
->label('Connections From')
|
||||
->default('%'),
|
||||
]),
|
||||
])
|
||||
@ -76,4 +76,9 @@ class ListDatabases extends ListRecords
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/database.title');
|
||||
}
|
||||
}
|
||||
|
@ -55,4 +55,9 @@ class FileResource extends Resource
|
||||
'index' => Pages\ListFiles::route('/{path?}'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/file.title');
|
||||
}
|
||||
}
|
||||
|
@ -69,10 +69,10 @@ class EditFiles extends Page
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('Editing: ' . $this->path)
|
||||
Section::make(trans('server/file.actions.edit.title', ['file' => $this->path]))
|
||||
->footerActions([
|
||||
Action::make('save_and_close')
|
||||
->label('Save & Close')
|
||||
->label(trans('server/file.actions.edit.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('File saved')
|
||||
->title(trans('server/file.actions.edit.notification'))
|
||||
->body(fn () => $this->path)
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
}),
|
||||
Action::make('save')
|
||||
->label('Save')
|
||||
->label(trans('server/file.actions.edit.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('File saved')
|
||||
->title(trans('server/file.actions.edit.notification'))
|
||||
->body(fn () => $this->path)
|
||||
->send();
|
||||
}),
|
||||
Action::make('cancel')
|
||||
->label('Cancel')
|
||||
->label(trans('server/file.actions.edit.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('Syntax Highlighting')
|
||||
->label(trans('server/file.actions.new_file.syntax'))
|
||||
->searchable()
|
||||
->native(false)
|
||||
->live()
|
||||
@ -133,7 +133,7 @@ class EditFiles extends Page
|
||||
try {
|
||||
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
|
||||
} catch (FileSizeTooLargeException) {
|
||||
AlertBanner::make()
|
||||
AlertBanner::make('file_too_large')
|
||||
->title('<code>' . basename($this->path) . '</code> is too large!')
|
||||
->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
|
||||
->danger()
|
||||
@ -142,7 +142,7 @@ class EditFiles extends Page
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
} catch (FileNotFoundException) {
|
||||
AlertBanner::make()
|
||||
AlertBanner::make('file_not_found')
|
||||
->title('<code>' . basename($this->path) . '</code> not found!')
|
||||
->danger()
|
||||
->closable()
|
||||
@ -150,7 +150,7 @@ class EditFiles extends Page
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
} catch (FileNotEditableException) {
|
||||
AlertBanner::make()
|
||||
AlertBanner::make('file_is_directory')
|
||||
->title('<code>' . basename($this->path) . '</code> is a directory')
|
||||
->danger()
|
||||
->closable()
|
||||
@ -184,15 +184,6 @@ class EditFiles extends Page
|
||||
->info()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
try {
|
||||
$this->getDaemonFileRepository()->getDirectory('/');
|
||||
} catch (ConnectionException) {
|
||||
AlertBanner::make('node_connection_error')
|
||||
->title('Could not connect to the node!')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,14 +90,17 @@ 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(),
|
||||
@ -116,7 +119,7 @@ class ListFiles extends ListRecords
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->label('Open')
|
||||
->label(trans('server/file.actions.open'))
|
||||
->icon('tabler-eye')
|
||||
->visible(fn (File $file) => $file->is_directory)
|
||||
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
@ -128,11 +131,11 @@ class ListFiles extends ListRecords
|
||||
ActionGroup::make([
|
||||
Action::make('rename')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Rename')
|
||||
->label(trans('server/file.actions.rename.title'))
|
||||
->icon('tabler-forms')
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('File name')
|
||||
->label(trans('server/file.actions.rename.name'))
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required(),
|
||||
])
|
||||
@ -149,14 +152,14 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File Renamed')
|
||||
->title(trans('server/file.actions.rename.notification'))
|
||||
->body(fn () => $file->name . ' -> ' . $data['name'])
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('copy')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('Copy')
|
||||
->label(trans('server/file.actions.copy.title'))
|
||||
->icon('tabler-copy')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->action(function (File $file) {
|
||||
@ -167,7 +170,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File copied')
|
||||
->title(trans('server/file.actions.copy.notification'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@ -175,18 +178,18 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->label('Download')
|
||||
->label(trans('server/file.actions.download'))
|
||||
->icon('tabler-download')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->url(fn (File $file) => DownloadFiles::getUrl(['path' => join_paths($this->path, $file->name)]), true),
|
||||
Action::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Move')
|
||||
->label(trans('server/file.actions.move.title'))
|
||||
->icon('tabler-replace')
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('New location')
|
||||
->hint('Enter the location of this file or folder, relative to the current directory.')
|
||||
->label(trans('server/file.actions.move.new_location'))
|
||||
->hint(trans('server/file.actions.move.new_location_hint'))
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
@ -209,22 +212,24 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File Moved')
|
||||
->title(trans('server/file.actions.move.notification'))
|
||||
->body($oldLocation . ' -> ' . $newLocation)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('permissions')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Permissions')
|
||||
->label(trans('server/file.actions.permissions.title'))
|
||||
->icon('tabler-license')
|
||||
->form([
|
||||
CheckboxList::make('owner')
|
||||
->label(trans('server/file.actions.permissions.owner'))
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
'read' => trans('server/file.actions.permissions.read'),
|
||||
'write' => trans('server/file.actions.permissions.write'),
|
||||
'execute' => trans('server/file.actions.permissions.execute'),
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 0, 1);
|
||||
@ -232,11 +237,13 @@ class ListFiles extends ListRecords
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
CheckboxList::make('group')
|
||||
->label(trans('server/file.actions.permissions.group'))
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
'read' => trans('server/file.actions.permissions.read'),
|
||||
'write' => trans('server/file.actions.permissions.write'),
|
||||
'execute' => trans('server/file.actions.permissions.execute'),
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 1, 1);
|
||||
@ -244,11 +251,13 @@ class ListFiles extends ListRecords
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
CheckboxList::make('public')
|
||||
->label(trans('server/file.actions.permissions.public'))
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
'read' => trans('server/file.actions.permissions.read'),
|
||||
'write' => trans('server/file.actions.permissions.write'),
|
||||
'execute' => trans('server/file.actions.permissions.execute'),
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 2, 1);
|
||||
@ -266,17 +275,17 @@ class ListFiles extends ListRecords
|
||||
$this->getDaemonFileRepository()->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
|
||||
|
||||
Notification::make()
|
||||
->title('Permissions changed to ' . $mode)
|
||||
->title(trans('server/file.actions.permissions.notification', ['mode' => $mode]))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->label('Archive')
|
||||
->label(trans('server/file.actions.archive.title'))
|
||||
->icon('tabler-archive')
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Archive name')
|
||||
->label(trans('server/file.actions.archive.archive_name'))
|
||||
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
||||
->suffix('.tar.gz'),
|
||||
])
|
||||
@ -290,7 +299,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->title(trans('server/file.actions.archive.notification'))
|
||||
->body($archive['name'])
|
||||
->success()
|
||||
->send();
|
||||
@ -299,7 +308,7 @@ class ListFiles extends ListRecords
|
||||
}),
|
||||
Action::make('unarchive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->label('Unarchive')
|
||||
->label(trans('server/file.actions.unarchive.title'))
|
||||
->icon('tabler-archive')
|
||||
->visible(fn (File $file) => $file->isArchive())
|
||||
->action(function (File $file) {
|
||||
@ -311,7 +320,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Unarchive completed')
|
||||
->title(trans('server/file.actions.unarchive.notification'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@ -339,8 +348,8 @@ class ListFiles extends ListRecords
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('Directory')
|
||||
->hint('Enter the new directory, relative to the current directory.')
|
||||
->label(trans('server/file.actions.move.directory'))
|
||||
->hint(trans('server/file.actions.move.directory_hint'))
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
@ -358,7 +367,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files were moved to ' . resolve_path(join_paths($this->path, $location)))
|
||||
->title(trans('server/file.actions.move.bulk_notification', ['count' => count($files), 'directory' => resolve_path(join_paths($this->path, $location))]))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
@ -366,7 +375,7 @@ class ListFiles extends ListRecords
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Archive name')
|
||||
->label(trans('server/file.actions.archive.archive_name'))
|
||||
->placeholder(fn () => 'archive-' . str(Carbon::now()->toRfc3339String())->replace(':', '')->before('+0000') . 'Z')
|
||||
->suffix('.tar.gz'),
|
||||
])
|
||||
@ -382,7 +391,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->title(trans('server/file.actions.archive.notification'))
|
||||
->body($archive['name'])
|
||||
->success()
|
||||
->send();
|
||||
@ -401,7 +410,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files deleted.')
|
||||
->title(trans('server/file.actions.delete.bulk_notification', ['count' => count($files)]))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
@ -417,10 +426,10 @@ class ListFiles extends ListRecords
|
||||
return [
|
||||
HeaderAction::make('new_file')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->tooltip('New File')
|
||||
->tooltip(trans('server/file.actions.new_file.title'))
|
||||
->hiddenLabel()->icon('tabler-file-plus')->iconButton()->iconSize(IconSize::Large)
|
||||
->color('primary')
|
||||
->modalSubmitActionLabel('Create')
|
||||
->modalSubmitActionLabel(trans('server/file.actions.new_file.create'))
|
||||
->action(function ($data) {
|
||||
$path = join_paths($this->path, $data['name']);
|
||||
try {
|
||||
@ -430,7 +439,7 @@ class ListFiles extends ListRecords
|
||||
->property('file', join_paths($path, $data['name']))
|
||||
->log();
|
||||
} catch (FileExistsException) {
|
||||
AlertBanner::make()
|
||||
AlertBanner::make('file_already_exists')
|
||||
->title('<code>' . $path . '</code> already exists!')
|
||||
->danger()
|
||||
->closable()
|
||||
@ -441,10 +450,10 @@ class ListFiles extends ListRecords
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('File Name')
|
||||
->label(trans('server/file.actions.new_file.file_name'))
|
||||
->required(),
|
||||
Select::make('lang')
|
||||
->label('Syntax Highlighting')
|
||||
->label(trans('server/file.actions.new_file.syntax'))
|
||||
->searchable()
|
||||
->native(false)
|
||||
->live()
|
||||
@ -460,7 +469,7 @@ 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('New Folder')
|
||||
->tooltip(trans('server/file.actions.new_folder.title'))
|
||||
->color('primary')
|
||||
->action(function ($data) {
|
||||
try {
|
||||
@ -471,7 +480,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
} catch (FileExistsException) {
|
||||
$path = join_paths($this->path, $data['name']);
|
||||
AlertBanner::make()
|
||||
AlertBanner::make('folder_already_exists')
|
||||
->title('<code>' . $path . '</code> already exists!')
|
||||
->danger()
|
||||
->closable()
|
||||
@ -482,13 +491,13 @@ class ListFiles extends ListRecords
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Folder Name')
|
||||
->label(trans('server/file.actions.new_folder.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('Upload')
|
||||
->tooltip(trans('server/file.actions.upload.title'))
|
||||
->color('success')
|
||||
->action(function ($data) {
|
||||
if (count($data['files']) > 0 && !isset($data['url'])) {
|
||||
@ -516,7 +525,7 @@ class ListFiles extends ListRecords
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->schema([
|
||||
Tab::make('Upload Files')
|
||||
Tab::make(trans('server/file.actions.upload.from_files'))
|
||||
->live()
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
@ -526,12 +535,12 @@ class ListFiles extends ListRecords
|
||||
->maxSize((int) round($server->node->upload_size * (config('panel.use_binary_prefix') ? 1.048576 * 1024 : 1000)))
|
||||
->multiple(),
|
||||
]),
|
||||
Tab::make('Upload From URL')
|
||||
Tab::make(trans('server/file.actions.upload.url'))
|
||||
->live()
|
||||
->disabled(fn (Get $get) => count($get('files')) > 0)
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->label(trans('server/file.actions.upload.url'))
|
||||
->url(),
|
||||
]),
|
||||
]),
|
||||
@ -539,14 +548,15 @@ class ListFiles extends ListRecords
|
||||
HeaderAction::make('search')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->tooltip('Global Search')
|
||||
->tooltip(trans('server/file.actions.global_search.title'))
|
||||
->color('primary')
|
||||
->icon('tabler-world-search')
|
||||
->modalHeading('Global Search')
|
||||
->modalSubmitActionLabel('Search')
|
||||
->modalHeading(trans('server/file.actions.global_search.title'))
|
||||
->modalSubmitActionLabel(trans('server/file.actions.global_search.search'))
|
||||
->form([
|
||||
TextInput::make('searchTerm')
|
||||
->placeholder('Enter a search term, e.g. *.txt')
|
||||
->label(trans('server/file.actions.global_search.search_term'))
|
||||
->placeholder(trans('server/file.actions.global_search.search_term_placeholder'))
|
||||
->required()
|
||||
->regex('/^[^*]*\*?[^*]*$/')
|
||||
->minValue(3),
|
||||
@ -594,4 +604,9 @@ class ListFiles extends ListRecords
|
||||
->where('path', '.*'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/file.title');
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ 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;
|
||||
|
||||
@ -23,8 +24,6 @@ class SearchFiles extends ListRecords
|
||||
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
protected static ?string $title = 'Global Search';
|
||||
|
||||
#[Locked]
|
||||
public string $searchTerm;
|
||||
|
||||
@ -37,7 +36,7 @@ class SearchFiles extends ListRecords
|
||||
|
||||
return [
|
||||
$resource::getUrl() => $resource::getBreadcrumb(),
|
||||
self::getUrl(['searchTerm' => $this->searchTerm]) => 'Search "' . $this->searchTerm . '"',
|
||||
self::getUrl(['searchTerm' => $this->searchTerm]) => trans('server/file.actions.global_search.search') . ' "' . $this->searchTerm . '"',
|
||||
];
|
||||
}
|
||||
|
||||
@ -51,10 +50,18 @@ 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'),
|
||||
BytesColumn::make('size')
|
||||
->label(trans('server/file.size'))
|
||||
->visibleFrom('md')
|
||||
->state(fn (File $file) => $file->size)
|
||||
->sortable(),
|
||||
DateTimeColumn::make('modified_at')
|
||||
->label(trans('server/file.modified_at'))
|
||||
->visibleFrom('md')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
@ -66,4 +73,9 @@ 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');
|
||||
}
|
||||
}
|
||||
|
@ -85,21 +85,20 @@ class ScheduleResource extends Resource
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(trans('server/schedule.name'))
|
||||
->columnSpanFull()
|
||||
->label('Schedule Name')
|
||||
->placeholder('A human readable identifier for this schedule.')
|
||||
->autocomplete(false)
|
||||
->required(),
|
||||
Toggle::make('only_when_online')
|
||||
->label('Only when Server is Online?')
|
||||
->hintIconTooltip('Only execute this schedule when the server is in a running state.')
|
||||
->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('Enable Schedule?')
|
||||
->hintIconTooltip('This schedule will be executed automatically if enabled.')
|
||||
->label(trans('server/schedule.enabled'))
|
||||
->hintIconTooltip(trans('server/schedule.enabled_hint'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->inline(false)
|
||||
->hiddenOn('view')
|
||||
@ -107,7 +106,7 @@ class ScheduleResource extends Resource
|
||||
->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' => 'Inactive'] : ($schedule->is_processing ? ['processing' => 'Processing'] : ['active' => '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')]))
|
||||
->colors([
|
||||
'inactive' => 'danger',
|
||||
'processing' => 'warning',
|
||||
@ -115,22 +114,35 @@ class ScheduleResource extends Resource
|
||||
])
|
||||
->visibleOn('view'),
|
||||
Section::make('Cron')
|
||||
->description(fn (Get $get) => new HtmlString('Please keep in mind that the cron inputs below always assume UTC.<br>Next run in your timezone (' . auth()->user()->timezone . '): <b>'. 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) . '</b>'))
|
||||
->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]));
|
||||
})
|
||||
->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('Weekly (Monday)')
|
||||
->label(trans('server/schedule.time.weekly_mon'))
|
||||
->cron('0', '0', '*', '*', '1'),
|
||||
CronPresetAction::make('weekly_sunday')
|
||||
->label('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') == '*'
|
||||
@ -142,8 +154,8 @@ class ScheduleResource extends Resource
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(60)
|
||||
->prefix('Every')
|
||||
->suffix('Minutes'),
|
||||
->prefix(trans('server/schedule.time.every'))
|
||||
->suffix(trans('server/schedule.time.minutes')),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '*/' . $data['x']);
|
||||
@ -164,8 +176,8 @@ class ScheduleResource extends Resource
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Hours'),
|
||||
->prefix(trans('server/schedule.time.every'))
|
||||
->suffix(trans('server/schedule.time.hours')),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
@ -186,8 +198,8 @@ class ScheduleResource extends Resource
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Days'),
|
||||
->prefix(trans('server/schedule.time.every'))
|
||||
->suffix(trans('server/schedule.time.days')),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
@ -208,8 +220,8 @@ class ScheduleResource extends Resource
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Months'),
|
||||
->prefix(trans('server/schedule.time.every'))
|
||||
->suffix(trans('server/schedule.time.months')),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
@ -227,15 +239,15 @@ class ScheduleResource extends Resource
|
||||
->form([
|
||||
Select::make('x')
|
||||
->label('')
|
||||
->prefix('Every')
|
||||
->prefix(trans('server/schedule.time.every'))
|
||||
->options([
|
||||
'1' => 'Monday',
|
||||
'2' => 'Tuesday',
|
||||
'3' => 'Wednesday',
|
||||
'4' => 'Thursday',
|
||||
'5' => 'Friday',
|
||||
'6' => 'Saturday',
|
||||
'0' => 'Sunday',
|
||||
'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'),
|
||||
])
|
||||
->selectablePlaceholder(false)
|
||||
->native(false),
|
||||
@ -251,47 +263,47 @@ class ScheduleResource extends Resource
|
||||
->hiddenOn('view'),
|
||||
Group::make([
|
||||
TextInput::make('cron_minute')
|
||||
->label(trans('server/schedule.time.minute'))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Minute')
|
||||
->default('*/5')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('cron_hour')
|
||||
->label(trans('server/schedule.time.hour'))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Hour')
|
||||
->default('*')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('cron_day_of_month')
|
||||
->label(trans('server/schedule.time.day_of_month'))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Day of Month')
|
||||
->default('*')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('cron_month')
|
||||
->label(trans('server/schedule.time.month'))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Month')
|
||||
->default('*')
|
||||
->required()
|
||||
->live(),
|
||||
TextInput::make('cron_day_of_week')
|
||||
->label(trans('server/schedule.time.day_of_week'))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'lg' => 1,
|
||||
])
|
||||
->label('Day of Week')
|
||||
->default('*')
|
||||
->required()
|
||||
->live(),
|
||||
@ -309,22 +321,26 @@ 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')
|
||||
->state(fn (Schedule $schedule) => !$schedule->is_active ? 'Inactive' : ($schedule->is_processing ? 'Processing' : 'Active')),
|
||||
->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'))),
|
||||
IconColumn::make('only_when_online')
|
||||
->label(trans('server/schedule.online_only'))
|
||||
->boolean()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('last_run_at')
|
||||
->label('Last run')
|
||||
->placeholder('Never')
|
||||
->label(trans('server/schedule.last_run'))
|
||||
->placeholder(trans('server/schedule.never'))
|
||||
->since()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('next_run_at')
|
||||
->label('Next run')
|
||||
->placeholder('Never')
|
||||
->label(trans('server/schedule.next_run'))
|
||||
->placeholder(trans('server/schedule.never'))
|
||||
->since()
|
||||
->sortable()
|
||||
->state(fn (Schedule $schedule) => $schedule->is_active ? $schedule->next_run_at : null),
|
||||
@ -367,11 +383,16 @@ class ScheduleResource extends Resource
|
||||
return Utilities::getScheduleNextRunDate($minute, $hour, $dayOfMonth, $month, $dayOfWeek);
|
||||
} catch (Exception) {
|
||||
Notification::make()
|
||||
->title('The cron data provided does not evaluate to a valid expression')
|
||||
->title(trans('server/schedule.notification_invalid_cron'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt();
|
||||
}
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/schedule.title');
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ class EditSchedule extends EditRecord
|
||||
Actions\DeleteAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-trash')
|
||||
->tooltip('Delete Schedule')
|
||||
->tooltip(trans('server/schedule.delete'))
|
||||
->after(function ($record) {
|
||||
Activity::event('server:schedule.delete')
|
||||
->property('name', $record->name)
|
||||
@ -58,15 +58,15 @@ class EditSchedule extends EditRecord
|
||||
ExportScheduleAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-download')
|
||||
->tooltip('Export Schedule'),
|
||||
->tooltip(trans('server/schedule.export')),
|
||||
$this->getSaveFormAction()->formId('form')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-device-floppy')
|
||||
->tooltip('Save Schedule'),
|
||||
->tooltip(trans('server/schedule.save')),
|
||||
$this->getCancelFormAction()->formId('form')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-cancel')
|
||||
->tooltip('Cancel'),
|
||||
->tooltip(trans('server/schedule.cancel')),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -26,11 +26,11 @@ class ListSchedules extends ListRecords
|
||||
CreateAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-calendar-plus')
|
||||
->tooltip('New Schedule'),
|
||||
->tooltip(trans('server/schedule.new')),
|
||||
ImportScheduleAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-download')
|
||||
->tooltip('Import Schedule'),
|
||||
->tooltip(trans('server/schedule.import')),
|
||||
];
|
||||
}
|
||||
|
||||
@ -38,4 +38,9 @@ class ListSchedules extends ListRecords
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/schedule.title');
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,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 ? 'No tasks' : ($schedule->is_processing ? 'Processing' : 'Run now'))
|
||||
->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')))
|
||||
->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) {
|
||||
@ -45,7 +45,7 @@ class ViewSchedule extends ViewRecord
|
||||
EditAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-calendar-code')
|
||||
->tooltip('Edit Schedule'),
|
||||
->tooltip(trans('server/schedule.edit')),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -30,10 +30,10 @@ class TasksRelationManager extends RelationManager
|
||||
private function getActionOptions(bool $full = true): array
|
||||
{
|
||||
return [
|
||||
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',
|
||||
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'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ 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)
|
||||
@ -53,27 +54,29 @@ 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')] ?? 'Payload'),
|
||||
->label(fn (Get $get) => $this->getActionOptions(false)[$get('action')] ?? trans('server/schedule.tasks.payload')),
|
||||
Select::make('payload')
|
||||
->visible(fn (Get $get) => $get('action') === Task::ACTION_POWER)
|
||||
->label('Power Action')
|
||||
->label(trans('server/schedule.tasks.actions.power.action'))
|
||||
->required()
|
||||
->options([
|
||||
'start' => 'Start',
|
||||
'restart' => 'Restart',
|
||||
'stop' => 'Stop',
|
||||
'kill' => 'Kill',
|
||||
'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'),
|
||||
])
|
||||
->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('Seconds'),
|
||||
Toggle::make('continue_on_failure'),
|
||||
->suffix(trans('server/schedule.tasks.seconds')),
|
||||
Toggle::make('continue_on_failure')
|
||||
->label(trans('server/schedule.tasks.continue_on_failure')),
|
||||
];
|
||||
}
|
||||
|
||||
@ -87,17 +90,21 @@ 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(' Seconds'),
|
||||
->suffix(' '. trans('server/schedule.tasks.seconds')),
|
||||
IconColumn::make('continue_on_failure')
|
||||
->label(trans('server/schedule.tasks.continue_on_failure'))
|
||||
->boolean(),
|
||||
])
|
||||
->actions([
|
||||
@ -133,7 +140,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) ? 'Task Limit Reached' : 'Create Task')
|
||||
->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'))
|
||||
->disabled(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10))
|
||||
->form($this->getTaskForm($schedule))
|
||||
->action(function ($data) use ($schedule) {
|
||||
|
@ -91,14 +91,14 @@ class UserResource extends Resource
|
||||
|
||||
foreach ($data['permissions'] as $permission) {
|
||||
$options[$permission] = str($permission)->headline();
|
||||
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
$descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
$permissionsArray[$data['name']][] = $permission;
|
||||
}
|
||||
|
||||
$tabs[] = Tab::make(str($data['name'])->headline())
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
|
||||
->description(trans('server/user.permissions.' . $data['name'] . '_desc'))
|
||||
->icon($data['icon'])
|
||||
->schema([
|
||||
CheckboxList::make($data['name'])
|
||||
@ -121,30 +121,33 @@ 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('Remove User')
|
||||
->label(trans('server/user.delete'))
|
||||
->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('User Deleted!')
|
||||
->title(trans('server/user.notification_delete'))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
EditAction::make()
|
||||
->label('Edit User')
|
||||
->label(trans('server/user.edit'))
|
||||
->hidden(fn (User $user) => auth()->user()->id === $user->id)
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_UPDATE, $server))
|
||||
->modalHeading(fn (User $user) => 'Editing ' . $user->email)
|
||||
->modalHeading(fn (User $user) => trans('server/user.editing', ['user' => $user->email]))
|
||||
->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) {
|
||||
$subuser = $server->subusers->where('user_id', $user->id)->first();
|
||||
|
||||
@ -158,7 +161,7 @@ class UserResource extends Resource
|
||||
$subuserUpdateService->handle($subuser, $server, $permissions);
|
||||
|
||||
Notification::make()
|
||||
->title('User Updated!')
|
||||
->title(trans('server/user.notification_edit'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@ -185,7 +188,7 @@ class UserResource extends Resource
|
||||
]),
|
||||
Actions::make([
|
||||
Action::make('assignAll')
|
||||
->label('Assign All')
|
||||
->label(trans('server/user.assign_all'))
|
||||
->action(function (Set $set) use ($permissionsArray) {
|
||||
$permissions = $permissionsArray;
|
||||
foreach ($permissions as $key => $value) {
|
||||
@ -231,4 +234,9 @@ class UserResource extends Resource
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/user.title');
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ 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
|
||||
{
|
||||
@ -48,14 +49,14 @@ class ListUsers extends ListRecords
|
||||
|
||||
foreach ($data['permissions'] as $permission) {
|
||||
$options[$permission] = str($permission)->headline();
|
||||
$descriptions[$permission] = trans('server/users.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
$descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
$permissionsArray[$data['name']][] = $permission;
|
||||
}
|
||||
|
||||
$tabs[] = Tab::make(str($data['name'])->headline())
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.' . $data['name'] . '_desc'))
|
||||
->description(trans('server/user.permissions.' . $data['name'] . '_desc'))
|
||||
->icon($data['icon'])
|
||||
->schema([
|
||||
CheckboxList::make($data['name'])
|
||||
@ -72,7 +73,7 @@ class ListUsers extends ListRecords
|
||||
Actions\CreateAction::make('invite')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-user-plus')
|
||||
->tooltip('Invite User')
|
||||
->tooltip(trans('server/user.invite_user'))
|
||||
->createAnother(false)
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_CREATE, $server))
|
||||
->form([
|
||||
@ -86,6 +87,7 @@ class ListUsers extends ListRecords
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('email')
|
||||
->label(trans('server/user.email'))
|
||||
->email()
|
||||
->inlineLabel()
|
||||
->columnSpan([
|
||||
@ -97,7 +99,7 @@ class ListUsers extends ListRecords
|
||||
->required(),
|
||||
assignAll::make([
|
||||
Action::make('assignAll')
|
||||
->label('Assign All')
|
||||
->label(trans('server/user.assign_all'))
|
||||
->action(function (Set $set, Get $get) use ($permissionsArray) {
|
||||
$permissions = $permissionsArray;
|
||||
foreach ($permissions as $key => $value) {
|
||||
@ -117,8 +119,8 @@ class ListUsers extends ListRecords
|
||||
->schema($tabs),
|
||||
]),
|
||||
])
|
||||
->modalHeading('Invite User')
|
||||
->modalSubmitActionLabel('Invite')
|
||||
->modalHeading(trans('server/user.invite_user'))
|
||||
->modalSubmitActionLabel(trans('server/user.action'))
|
||||
->action(function (array $data, SubuserCreationService $service) use ($server) {
|
||||
$email = strtolower($data['email']);
|
||||
|
||||
@ -140,12 +142,12 @@ class ListUsers extends ListRecords
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('User Invited!')
|
||||
->title(trans('server/user.notification_add'))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Failed')
|
||||
->title(trans('server/user.notification_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
@ -160,4 +162,9 @@ class ListUsers extends ListRecords
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return trans('server/user.title');
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +80,6 @@ class ServerCpuChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'CPU';
|
||||
return trans('server/console.labels.cpu');
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +80,6 @@ class ServerMemoryChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'Memory';
|
||||
return trans('server/console.labels.memory');
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,6 @@ class ServerNetworkChart extends ChartWidget
|
||||
{
|
||||
$lastData = collect(cache()->get("servers.{$this->server->id}.network"))->last();
|
||||
|
||||
return 'Network - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0);
|
||||
return trans('server/console.labels.network') . ' - ↓' . convert_bytes_to_readable($lastData->rx_bytes ?? 0) . ' - ↑' . convert_bytes_to_readable($lastData->tx_bytes ?? 0);
|
||||
}
|
||||
}
|
||||
|
@ -20,14 +20,14 @@ class ServerOverview extends StatsOverviewWidget
|
||||
protected function getStats(): array
|
||||
{
|
||||
return [
|
||||
SmallStatBlock::make('Name', $this->server->name)
|
||||
SmallStatBlock::make(trans('server/console.labels.name'), $this->server->name)
|
||||
->copyOnClick(fn () => request()->isSecure()),
|
||||
SmallStatBlock::make('Status', $this->status()),
|
||||
SmallStatBlock::make('Address', $this->server?->allocation->address ?? 'None')
|
||||
SmallStatBlock::make(trans('server/console.labels.status'), $this->status()),
|
||||
SmallStatBlock::make(trans('server/console.labels.address'), $this->server?->allocation->address ?? 'None')
|
||||
->copyOnClick(fn () => request()->isSecure()),
|
||||
SmallStatBlock::make('CPU', $this->cpuUsage()),
|
||||
SmallStatBlock::make('Memory', $this->memoryUsage()),
|
||||
SmallStatBlock::make('Disk', $this->diskUsage()),
|
||||
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()),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ 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')]
|
||||
@ -195,6 +196,35 @@ 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
|
||||
*
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Remote;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\User;
|
||||
@ -14,7 +15,7 @@ class ActivityProcessingController extends Controller
|
||||
{
|
||||
public function __invoke(ActivityEventRequest $request): void
|
||||
{
|
||||
/** @var \App\Models\Node $node */
|
||||
/** @var Node $node */
|
||||
$node = $request->attributes->get('node');
|
||||
|
||||
$servers = $node->servers()->whereIn('uuid', $request->servers())->get()->keyBy('uuid');
|
||||
@ -22,7 +23,7 @@ class ActivityProcessingController extends Controller
|
||||
|
||||
$logs = [];
|
||||
foreach ($request->input('data') as $datum) {
|
||||
/** @var \App\Models\Server|null $server */
|
||||
/** @var Server|null $server */
|
||||
$server = $servers->get($datum['server']);
|
||||
if (is_null($server) || !Str::startsWith($datum['event'], 'server:')) {
|
||||
continue;
|
||||
|
@ -2,7 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Http\Requests\Api\Remote\ServerRequest;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
@ -12,11 +13,11 @@ class ServerContainersController extends Controller
|
||||
/**
|
||||
* Updates the server container's status on the Panel
|
||||
*/
|
||||
public function status(Server $server, Request $request): JsonResponse
|
||||
public function status(ServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$status = fluent($request->json()->all())->get('data.new_state');
|
||||
$status = ContainerStatus::tryFrom($request->json('data.new_state')) ?? ContainerStatus::Missing;
|
||||
|
||||
cache()->put("servers.$server->uuid.container.status", $status, now()->addHour());
|
||||
cache()->put("servers.$server->uuid.status", $status, now()->addHour());
|
||||
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
@ -3,7 +3,10 @@
|
||||
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;
|
||||
@ -29,7 +32,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(Server $server): JsonResponse
|
||||
public function __invoke(ServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
return new JsonResponse([
|
||||
'settings' => $this->configurationStructureService->handle($server),
|
||||
@ -42,7 +45,7 @@ class ServerDetailsController extends Controller
|
||||
*/
|
||||
public function list(Request $request): ServerConfigurationCollection
|
||||
{
|
||||
/** @var \App\Models\Node $node */
|
||||
/** @var Node $node */
|
||||
$node = $request->attributes->get('node');
|
||||
|
||||
// Avoid run-away N+1 SQL queries by preloading the relationships that are used
|
||||
@ -85,9 +88,9 @@ class ServerDetailsController extends Controller
|
||||
->get();
|
||||
|
||||
$this->connection->transaction(function () use ($node, $servers) {
|
||||
/** @var \App\Models\Server $server */
|
||||
/** @var Server $server */
|
||||
foreach ($servers as $server) {
|
||||
/** @var \App\Models\ActivityLog|null $activity */
|
||||
/** @var ActivityLog|null $activity */
|
||||
$activity = $server->activity->first();
|
||||
if (!$activity) {
|
||||
continue;
|
||||
|
@ -3,6 +3,7 @@
|
||||
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;
|
||||
@ -15,14 +16,12 @@ class ServerInstallController extends Controller
|
||||
/**
|
||||
* Returns installation information for a server.
|
||||
*/
|
||||
public function index(Server $server): JsonResponse
|
||||
public function index(ServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$egg = $server->egg;
|
||||
|
||||
return new JsonResponse([
|
||||
'container_image' => $egg->copy_script_container,
|
||||
'entrypoint' => $egg->copy_script_entry,
|
||||
'script' => $egg->copy_script_install,
|
||||
'container_image' => $server->egg->copy_script_container,
|
||||
'entrypoint' => $server->egg->copy_script_entry,
|
||||
'script' => $server->egg->copy_script_install,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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,14 +28,23 @@ class ServerTransferController extends Controller
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function failure(Server $server): JsonResponse
|
||||
public function failure(ServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$transfer = $server->transfer;
|
||||
if (is_null($transfer)) {
|
||||
throw new ConflictHttpException('Server is not being transferred.');
|
||||
}
|
||||
|
||||
return $this->processFailedTransfer($transfer);
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,16 +52,17 @@ class ServerTransferController extends Controller
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function success(Server $server): JsonResponse
|
||||
public function success(ServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$transfer = $server->transfer;
|
||||
if (is_null($transfer)) {
|
||||
throw new ConflictHttpException('Server is not being transferred.');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
/** @var \App\Models\Server $server */
|
||||
$server = $this->connection->transaction(function () use ($server, $transfer, $data) {
|
||||
/** @var 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
|
||||
@ -60,6 +70,7 @@ 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);
|
||||
|
||||
@ -82,24 +93,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
@ -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\UserUpdateService;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Auth\AuthManager;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Socialite\Contracts\User as OAuthUser;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;
|
||||
|
||||
class OAuthController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthManager $auth,
|
||||
private readonly UserUpdateService $updateService,
|
||||
private readonly OAuthService $oauthService
|
||||
private readonly UserCreationService $userCreation,
|
||||
private readonly OAuthService $oauthService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Redirect user to the OAuth provider
|
||||
*/
|
||||
public function redirect(string $driver): RedirectResponse
|
||||
public function redirect(string $driver): SymfonyRedirectResponse|RedirectResponse
|
||||
{
|
||||
// Driver is disabled - redirect to normal login
|
||||
if (!$this->oauthService->get($driver)->isEnabled()) {
|
||||
return redirect()->route('auth.login');
|
||||
}
|
||||
|
||||
return Socialite::with($driver)->redirect();
|
||||
return Socialite::driver($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -40,8 +40,9 @@ class OAuthController extends Controller
|
||||
*/
|
||||
public function callback(Request $request, string $driver): RedirectResponse
|
||||
{
|
||||
// Driver is disabled - redirect to normal login
|
||||
if (!$this->oauthService->get($driver)?->isEnabled()) {
|
||||
$driver = $this->oauthService->get($driver);
|
||||
|
||||
if (!$driver || !$driver->isEnabled()) {
|
||||
return redirect()->route('auth.login');
|
||||
}
|
||||
|
||||
@ -49,43 +50,89 @@ class OAuthController extends Controller
|
||||
if ($request->get('error')) {
|
||||
report($request->get('error_description') ?? $request->get('error'));
|
||||
|
||||
Notification::make()
|
||||
->title('Something went wrong')
|
||||
->body($request->get('error'))
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
return redirect()->route('auth.login');
|
||||
return $this->errorRedirect($request->get('error'));
|
||||
}
|
||||
|
||||
$oauthUser = Socialite::driver($driver)->user();
|
||||
$oauthUser = Socialite::driver($driver->getId())->user();
|
||||
|
||||
// User is already logged in and wants to link a new OAuth Provider
|
||||
if ($request->user()) {
|
||||
$oauth = $request->user()->oauth;
|
||||
$oauth[$driver] = $oauthUser->getId();
|
||||
|
||||
$this->updateService->handle($request->user(), ['oauth' => $oauth]);
|
||||
$this->linkUser($request->user(), $driver, $oauthUser);
|
||||
|
||||
return redirect(EditProfile::getUrl(['tab' => '-oauth-tab'], panel: 'app'));
|
||||
}
|
||||
|
||||
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');
|
||||
$user = User::whereJsonContains('oauth->'. $driver->getId(), $oauthUser->getId())->first();
|
||||
if ($user) {
|
||||
return $this->loginUser($user);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
@ -2,15 +2,8 @@
|
||||
|
||||
namespace App\Http\Requests\Api\Remote;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InstallationDataRequest extends FormRequest
|
||||
class InstallationDataRequest extends ServerRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]>
|
||||
*/
|
||||
|
21
app/Http/Requests/Api/Remote/ServerRequest.php
Normal file
21
app/Http/Requests/Api/Remote/ServerRequest.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@ -28,14 +28,14 @@ class ProcessWebhook implements ShouldQueue
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$data = $this->data[0];
|
||||
$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);
|
||||
|
||||
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,9 +53,10 @@ class ProcessWebhook implements ShouldQueue
|
||||
}
|
||||
|
||||
try {
|
||||
$customHeaders = $this->webhookConfiguration->headers;
|
||||
$headers = [];
|
||||
if ($this->webhookConfiguration->type === WebhookType::Regular && $customHeaders = $this->webhookConfiguration->headers) {
|
||||
$headers = array_merge(['X-Webhook-Event', $this->eventName], $customHeaders);
|
||||
foreach ($customHeaders as $key => $value) {
|
||||
$headers[$key] = $this->webhookConfiguration->replaceVars($data, $value);
|
||||
}
|
||||
|
||||
Http::withHeaders($headers)->post($this->webhookConfiguration->endpoint, $data)->throw();
|
||||
|
@ -4,25 +4,32 @@ namespace App\Livewire;
|
||||
|
||||
use Closure;
|
||||
use Filament\Notifications\Concerns;
|
||||
use Filament\Support\Concerns\EvaluatesClosures;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Wireable;
|
||||
use Filament\Support\Components\ViewComponent;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
|
||||
final class AlertBanner implements Wireable
|
||||
final class AlertBanner extends ViewComponent implements Arrayable
|
||||
{
|
||||
use Concerns\HasBody;
|
||||
use Concerns\HasIcon;
|
||||
use Concerns\HasId;
|
||||
use Concerns\HasStatus;
|
||||
use Concerns\HasTitle;
|
||||
use EvaluatesClosures;
|
||||
|
||||
protected bool|Closure $closable = false;
|
||||
|
||||
public static function make(?string $id = null): AlertBanner
|
||||
protected string $view = 'livewire.alerts.alert-banner';
|
||||
|
||||
protected string $viewIdentifier = 'alert-banner';
|
||||
|
||||
public function __construct(string $id)
|
||||
{
|
||||
$static = new self();
|
||||
$static->id($id ?? Str::orderedUuid());
|
||||
$this->id($id);
|
||||
}
|
||||
|
||||
public static function make(string $id): AlertBanner
|
||||
{
|
||||
$static = new self($id);
|
||||
$static->configure();
|
||||
|
||||
return $static;
|
||||
}
|
||||
@ -30,7 +37,7 @@ final class AlertBanner implements Wireable
|
||||
/**
|
||||
* @return array{id: string, title: ?string, body: ?string, status: ?string, icon: ?string, closeable: bool}
|
||||
*/
|
||||
public function toLivewire(): array
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
@ -42,15 +49,18 @@ final class AlertBanner implements Wireable
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromLivewire(mixed $value): AlertBanner
|
||||
/**
|
||||
* @param array{id: string, title: ?string, body: ?string, status: ?string, icon: ?string, closeable: bool} $data
|
||||
*/
|
||||
public static function fromArray(array $data): AlertBanner
|
||||
{
|
||||
$static = AlertBanner::make($value['id']);
|
||||
$static = AlertBanner::make($data['id']);
|
||||
|
||||
$static->title($value['title']);
|
||||
$static->body($value['body']);
|
||||
$static->status($value['status']);
|
||||
$static->icon($value['icon']);
|
||||
$static->closable($value['closeable']);
|
||||
$static->title($data['title']);
|
||||
$static->body($data['body']);
|
||||
$static->status($data['status']);
|
||||
$static->icon($data['icon']);
|
||||
$static->closable($data['closeable']);
|
||||
|
||||
return $static;
|
||||
}
|
||||
@ -69,7 +79,7 @@ final class AlertBanner implements Wireable
|
||||
|
||||
public function send(): AlertBanner
|
||||
{
|
||||
session()->push('alert-banners', $this->toLivewire());
|
||||
session()->push('alert-banners', $this->toArray());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -2,18 +2,18 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Filament\Notifications\Collection;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class AlertBannerContainer extends Component
|
||||
{
|
||||
/** @var array<AlertBanner> */
|
||||
public array $alertBanners;
|
||||
public Collection $alertBanners;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->alertBanners = [];
|
||||
$this->alertBanners = new Collection();
|
||||
$this->pullFromSession();
|
||||
}
|
||||
|
||||
@ -21,15 +21,16 @@ class AlertBannerContainer extends Component
|
||||
public function pullFromSession(): void
|
||||
{
|
||||
foreach (session()->pull('alert-banners', []) as $alertBanner) {
|
||||
$alertBanner = AlertBanner::fromLivewire($alertBanner);
|
||||
$this->alertBanners[$alertBanner->getId()] = $alertBanner;
|
||||
$alertBanner = AlertBanner::fromArray($alertBanner);
|
||||
$this->alertBanners->put($alertBanner->getId(), $alertBanner);
|
||||
}
|
||||
}
|
||||
|
||||
public function remove(string $id): void
|
||||
{
|
||||
$alertBanners = &$this->alertBanners;
|
||||
unset($alertBanners[$id]);
|
||||
if ($this->alertBanners->has($id)) {
|
||||
$this->alertBanners->forget($id);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
@ -24,37 +24,40 @@ class ServerEntry extends Component
|
||||
style="background-color: #D97706;">
|
||||
</div>
|
||||
|
||||
<div class="flex-1 dark:bg-gray-850 dark:text-white rounded-lg overflow-hidden p-2">
|
||||
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
|
||||
<div class="flex items-center mb-5 gap-2">
|
||||
<x-filament::loading-indicator class="h-5 w-5" />
|
||||
<x-filament::loading-indicator class="h-6 w-6" />
|
||||
<h2 class="text-xl font-bold">
|
||||
{{ $server->name }}
|
||||
<span class="dark:text-gray-400">
|
||||
({{ trans('server/dashboard.loading') }})
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-center">
|
||||
<div class="flex justify-between text-center items-center gap-4">
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">CPU</p>
|
||||
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.cpu') }}</p>
|
||||
<p class="text-md font-semibold">{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('cpu', type: \App\Enums\ServerResourceType::Percentage, limit: true) }}</p>
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::CPULimit) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">Memory</p>
|
||||
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.memory') }}</p>
|
||||
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('memory', limit: true) }}</p>
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::MemoryLimit) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm dark:text-gray-400">Disk</p>
|
||||
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.disk') }}</p>
|
||||
<p class="text-md font-semibold">{{ convert_bytes_to_readable(0, decimals: 2) }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('disk', limit: true) }}</p>
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource(\App\Enums\ServerResourceType::DiskLimit) }}</p>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<p class="text-sm dark:text-gray-400">Network</p>
|
||||
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.network') }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-md font-semibold">{{ $server->allocation?->address ?? 'None' }} </p>
|
||||
<p class="text-md font-semibold">{{ $server->allocation?->address ?? trans('server/dashboard.none') }} </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -69,7 +69,7 @@ class Egg extends Model implements Validatable
|
||||
/**
|
||||
* Defines the current egg export version.
|
||||
*/
|
||||
public const EXPORT_VERSION = 'PLCN_v1';
|
||||
public const EXPORT_VERSION = 'PLCN_v2';
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
|
@ -194,7 +194,7 @@ class File extends Model
|
||||
$message = str('Node connection failed');
|
||||
}
|
||||
|
||||
AlertBanner::make()
|
||||
AlertBanner::make('files_node_error')
|
||||
->title('Could not load files!')
|
||||
->body($message->toString())
|
||||
->danger()
|
||||
|
@ -462,17 +462,15 @@ class Server extends Model implements Validatable
|
||||
});
|
||||
}
|
||||
|
||||
public function formatResource(string $resourceKey, bool $limit = false, ServerResourceType $type = ServerResourceType::Unit, int $precision = 2): string
|
||||
public function formatResource(ServerResourceType $resourceType): string
|
||||
{
|
||||
$resourceAmount = $this->{$resourceKey} ?? 0;
|
||||
if (!$limit) {
|
||||
$resourceAmount = $this->retrieveResources()[$resourceKey] ?? 0;
|
||||
}
|
||||
$resourceAmount = $resourceType->getResourceAmount($this);
|
||||
|
||||
if ($type === ServerResourceType::Time) {
|
||||
if ($this->isSuspended()) {
|
||||
return 'Suspended';
|
||||
if ($resourceType->isTime()) {
|
||||
if (!is_null($this->status)) {
|
||||
return $this->status->getLabel();
|
||||
}
|
||||
|
||||
if ($resourceAmount === 0) {
|
||||
return ContainerStatus::Offline->getLabel();
|
||||
}
|
||||
@ -480,20 +478,16 @@ class Server extends Model implements Validatable
|
||||
return now()->subMillis($resourceAmount)->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE, short: true, parts: 4);
|
||||
}
|
||||
|
||||
if ($resourceAmount === 0 & $limit) {
|
||||
if ($resourceAmount === 0 & $resourceType->isLimit()) {
|
||||
// Unlimited symbol
|
||||
return "\u{221E}";
|
||||
}
|
||||
|
||||
if ($type === ServerResourceType::Percentage) {
|
||||
return Number::format($resourceAmount, precision: $precision, locale: auth()->user()->language ?? 'en') . '%';
|
||||
if ($resourceType->isPercentage()) {
|
||||
return Number::format($resourceAmount, precision: 2, locale: auth()->user()->language ?? 'en') . '%';
|
||||
}
|
||||
|
||||
// Our current limits are set in MB
|
||||
if ($limit) {
|
||||
$resourceAmount *= 2 ** 20;
|
||||
}
|
||||
|
||||
return convert_bytes_to_readable($resourceAmount, decimals: $precision, base: 3);
|
||||
return convert_bytes_to_readable($resourceAmount, base: 3);
|
||||
}
|
||||
|
||||
public function condition(): Attribute
|
||||
|
@ -101,24 +101,24 @@ class AppServiceProvider extends ServiceProvider
|
||||
'blurple' => Color::hex('#5865F2'),
|
||||
]);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::HEAD_START,
|
||||
fn () => Blade::render('filament.layouts.header')
|
||||
);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::PAGE_START,
|
||||
fn () => Blade::render('@livewire(\App\Livewire\AlertBannerContainer::class)'),
|
||||
);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::BODY_END,
|
||||
fn () => Blade::render('filament.layouts.body-end'),
|
||||
PanelsRenderHook::FOOTER,
|
||||
fn () => Blade::render('filament.layouts.footer'),
|
||||
);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::FOOTER,
|
||||
fn () => Blade::render('filament.layouts.footer'),
|
||||
PanelsRenderHook::STYLES_BEFORE,
|
||||
fn () => Blade::render("@vite(['resources/css/app.css'])")
|
||||
);
|
||||
|
||||
FilamentView::registerRenderHook(
|
||||
PanelsRenderHook::SCRIPTS_AFTER,
|
||||
fn () => Blade::render("@vite(['resources/js/app.js'])"),
|
||||
);
|
||||
|
||||
on('dehydrate', function (Component $component) {
|
||||
|
@ -2,45 +2,21 @@
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\Pages\Auth\Login;
|
||||
use App\Filament\Pages\Auth\EditProfile;
|
||||
use App\Http\Middleware\LanguageMiddleware;
|
||||
use App\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use Filament\Navigation\NavigationGroup;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class AdminPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->default()
|
||||
return parent::panel($panel)
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->homeUrl('/')
|
||||
->spa()
|
||||
->databaseNotifications()
|
||||
->breadcrumbs(false)
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
->brandLogoHeight('2rem')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
->topNavigation(config('panel.filament.top-navigation', false))
|
||||
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
|
||||
->login(Login::class)
|
||||
->passwordReset()
|
||||
->sidebarCollapsibleOnDesktop()
|
||||
->userMenuItems([
|
||||
'profile' => MenuItem::make()
|
||||
->label(fn () => trans('filament-panels::pages/auth/edit-profile.label'))
|
||||
@ -58,25 +34,8 @@ class AdminPanelProvider extends PanelProvider
|
||||
->collapsible(false),
|
||||
NavigationGroup::make(fn () => trans('admin/dashboard.advanced')),
|
||||
])
|
||||
->sidebarCollapsibleOnDesktop()
|
||||
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
|
||||
->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets')
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
LanguageMiddleware::class,
|
||||
RequireTwoFactorAuthentication::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets');
|
||||
}
|
||||
}
|
||||
|
@ -2,68 +2,27 @@
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\Pages\Auth\Login;
|
||||
use App\Filament\Pages\Auth\EditProfile;
|
||||
use App\Http\Middleware\LanguageMiddleware;
|
||||
use App\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class AppPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
return parent::panel($panel)
|
||||
->id('app')
|
||||
->spa()
|
||||
->databaseNotifications()
|
||||
->default()
|
||||
->breadcrumbs(false)
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
->brandLogoHeight('2rem')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
->topNavigation(config('panel.filament.top-navigation', false))
|
||||
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
|
||||
->navigation(false)
|
||||
->profile(EditProfile::class, false)
|
||||
->login(Login::class)
|
||||
->passwordReset()
|
||||
->userMenuItems([
|
||||
MenuItem::make()
|
||||
->label('Admin')
|
||||
->label(trans('profile.admin'))
|
||||
->url('/admin')
|
||||
->icon('tabler-arrow-forward')
|
||||
->sort(5)
|
||||
->visible(fn (): bool => auth()->user()->canAccessPanel(Filament::getPanel('admin'))),
|
||||
->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin'))),
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/App/Resources'), for: 'App\\Filament\\App\\Resources')
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
LanguageMiddleware::class,
|
||||
RequireTwoFactorAuthentication::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
->discoverResources(in: app_path('Filament/App/Resources'), for: 'App\\Filament\\App\\Resources');
|
||||
}
|
||||
}
|
||||
|
55
app/Providers/Filament/PanelProvider.php
Normal file
55
app/Providers/Filament/PanelProvider.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\Pages\Auth\Login;
|
||||
use App\Filament\Pages\Auth\EditProfile;
|
||||
use App\Http\Middleware\LanguageMiddleware;
|
||||
use App\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider as BasePanelProvider;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
abstract class PanelProvider extends BasePanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
->spa()
|
||||
->databaseNotifications()
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
->brandLogoHeight('2rem')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
->topNavigation(fn () => auth()->user()->getCustomization()['top_navigation'] ?? false)
|
||||
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
|
||||
->profile(EditProfile::class, false)
|
||||
->login(Login::class)
|
||||
->passwordReset()
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
LanguageMiddleware::class,
|
||||
RequireTwoFactorAuthentication::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
}
|
||||
}
|
@ -3,48 +3,24 @@
|
||||
namespace App\Providers\Filament;
|
||||
|
||||
use App\Filament\App\Resources\ServerResource\Pages\ListServers;
|
||||
use App\Filament\Pages\Auth\Login;
|
||||
use App\Filament\Admin\Resources\ServerResource\Pages\EditServer;
|
||||
use App\Filament\Pages\Auth\EditProfile;
|
||||
use App\Http\Middleware\Activity\ServerSubject;
|
||||
use App\Http\Middleware\LanguageMiddleware;
|
||||
use App\Http\Middleware\RequireTwoFactorAuthentication;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use Filament\Navigation\NavigationItem;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
|
||||
class ServerPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
return $panel
|
||||
return parent::panel($panel)
|
||||
->id('server')
|
||||
->path('server')
|
||||
->homeUrl('/')
|
||||
->spa()
|
||||
->databaseNotifications()
|
||||
->tenant(Server::class)
|
||||
->brandName(config('app.name', 'Pelican'))
|
||||
->brandLogo(config('app.logo'))
|
||||
->brandLogoHeight('2rem')
|
||||
->favicon(config('app.favicon', '/pelican.ico'))
|
||||
->topNavigation(config('panel.filament.top-navigation', false))
|
||||
->maxContentWidth(config('panel.filament.display-width', 'screen-2xl'))
|
||||
->login(Login::class)
|
||||
->passwordReset()
|
||||
->userMenuItems([
|
||||
'profile' => MenuItem::make()
|
||||
->label(fn () => trans('filament-panels::pages/auth/edit-profile.label'))
|
||||
@ -55,14 +31,14 @@ class ServerPanelProvider extends PanelProvider
|
||||
->url(fn () => ListServers::getUrl(panel: 'app'))
|
||||
->sort(6),
|
||||
MenuItem::make()
|
||||
->label('Admin')
|
||||
->label(trans('profile.admin'))
|
||||
->icon('tabler-arrow-forward')
|
||||
->url(fn () => Filament::getPanel('admin')->getUrl())
|
||||
->sort(5)
|
||||
->visible(fn (): bool => auth()->user()->canAccessPanel(Filament::getPanel('admin'))),
|
||||
->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin'))),
|
||||
])
|
||||
->navigationItems([
|
||||
NavigationItem::make('Open in Admin')
|
||||
NavigationItem::make(trans('server/console.open_in_admin'))
|
||||
->url(fn () => EditServer::getUrl(['record' => Filament::getTenant()], panel: 'admin'))
|
||||
->visible(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin')) && auth()->user()->can('view server', Filament::getTenant()))
|
||||
->icon('tabler-arrow-back')
|
||||
@ -72,21 +48,7 @@ class ServerPanelProvider extends PanelProvider
|
||||
->discoverPages(in: app_path('Filament/Server/Pages'), for: 'App\\Filament\\Server\\Pages')
|
||||
->discoverWidgets(in: app_path('Filament/Server/Widgets'), for: 'App\\Filament\\Server\\Widgets')
|
||||
->middleware([
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
AuthenticateSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
LanguageMiddleware::class,
|
||||
RequireTwoFactorAuthentication::class,
|
||||
ServerSubject::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,14 @@ class EggConfigurationService
|
||||
* @return array{
|
||||
* startup: array{done: string[], user_interaction: string[], strip_ansi: bool},
|
||||
* stop: array{type: string, value: string},
|
||||
* configs: array<mixed>
|
||||
* configs: list<array{
|
||||
* file: string,
|
||||
* replace: list<array{
|
||||
* match: string,
|
||||
* if_value?: string,
|
||||
* replace_with: string
|
||||
* }>
|
||||
* }>
|
||||
* }
|
||||
*/
|
||||
public function handle(Server $server): array
|
||||
@ -81,9 +88,10 @@ class EggConfigurationService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
* @param array<string, mixed> $configs
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function replacePlaceholders(Server $server, object $configs): array
|
||||
protected function replacePlaceholders(Server $server, object|array $configs): array
|
||||
{
|
||||
// Get the legacy configuration structure for the server so that we
|
||||
// can property map the egg placeholders to values.
|
||||
|
@ -2,17 +2,19 @@
|
||||
|
||||
namespace App\Services\Eggs\Sharing;
|
||||
|
||||
use App\Enums\EggFormat;
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Egg;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\EggVariable;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class EggExporterService
|
||||
{
|
||||
/**
|
||||
* Return a JSON representation of an egg and its variables.
|
||||
* Return a JSON or YAML representation of an egg and its variables.
|
||||
*/
|
||||
public function handle(int $egg): string
|
||||
public function handle(int $egg, EggFormat $format): string
|
||||
{
|
||||
$egg = Egg::with(['scriptFrom', 'configFrom', 'variables'])->findOrFail($egg);
|
||||
|
||||
@ -30,9 +32,7 @@ class EggExporterService
|
||||
'tags' => $egg->tags,
|
||||
'features' => $egg->features,
|
||||
'docker_images' => $egg->docker_images,
|
||||
'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(function ($value) {
|
||||
return !empty($value);
|
||||
}),
|
||||
'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(fn ($v) => !empty($v))->values(),
|
||||
'startup' => $egg->startup,
|
||||
'config' => [
|
||||
'files' => $egg->inherit_config_files,
|
||||
@ -50,9 +50,50 @@ class EggExporterService
|
||||
'variables' => $egg->variables->map(function (EggVariable $eggVariable) {
|
||||
return Collection::make($eggVariable->toArray())
|
||||
->except(['id', 'egg_id', 'created_at', 'updated_at']);
|
||||
}),
|
||||
})->values()->toArray(),
|
||||
];
|
||||
|
||||
return json_encode($struct, JSON_PRETTY_PRINT);
|
||||
return match ($format) {
|
||||
EggFormat::JSON => json_encode($struct, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
|
||||
EggFormat::YAML => Yaml::dump($this->yamlExport($struct), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK | Yaml::DUMP_OBJECT_AS_MAP),
|
||||
};
|
||||
}
|
||||
|
||||
protected function yamlExport(mixed $data): mixed
|
||||
{
|
||||
if ($data instanceof Collection) {
|
||||
$data = $data->all();
|
||||
}
|
||||
|
||||
if (is_string($data)) {
|
||||
$decoded = json_decode($data, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $this->yamlExport($decoded);
|
||||
}
|
||||
|
||||
return str_replace(["\r\n", '\\r\\n', '\\n'], "\n", $data);
|
||||
}
|
||||
|
||||
if (is_array($data)) {
|
||||
$result = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (
|
||||
is_string($value) &&
|
||||
strtolower($key) === 'description' &&
|
||||
(str_contains($value, "\n") || strlen($value) > 80)
|
||||
) {
|
||||
$value = wordwrap($value, 100, "\n");
|
||||
} else {
|
||||
$value = $this->yamlExport($value);
|
||||
}
|
||||
|
||||
$result[$key] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Eggs\Sharing;
|
||||
|
||||
use App\Exceptions\Service\InvalidFileUploadException;
|
||||
use JsonException;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Models\Egg;
|
||||
@ -11,6 +12,9 @@ use App\Models\EggVariable;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
use stdClass;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
|
||||
class EggImporterService
|
||||
{
|
||||
@ -28,9 +32,9 @@ class EggImporterService
|
||||
public function __construct(protected ConnectionInterface $connection) {}
|
||||
|
||||
/**
|
||||
* Take an uploaded JSON file and parse it into a new egg.
|
||||
* Take an uploaded JSON or YAML file and parse it into a new egg.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
* @throws InvalidFileUploadException|Throwable
|
||||
*/
|
||||
public function fromFile(UploadedFile $file, ?Egg $egg = null): Egg
|
||||
{
|
||||
@ -46,7 +50,6 @@ class EggImporterService
|
||||
'copy_script_from' => null,
|
||||
]);
|
||||
|
||||
// Don't check for this anymore
|
||||
for ($i = 0; $i < count($parsed['variables']); $i++) {
|
||||
unset($parsed['variables'][$i]['field_type']);
|
||||
}
|
||||
@ -54,7 +57,6 @@ class EggImporterService
|
||||
$egg = $this->fillFromParsed($egg, $parsed);
|
||||
$egg->save();
|
||||
|
||||
// Update existing variables or create new ones.
|
||||
foreach ($parsed['variables'] ?? [] as $variable) {
|
||||
EggVariable::unguarded(function () use ($egg, $variable) {
|
||||
$variable['rules'] = is_array($variable['rules']) ? $variable['rules'] : explode('|', $variable['rules']);
|
||||
@ -66,7 +68,6 @@ class EggImporterService
|
||||
}
|
||||
|
||||
$imported = array_map(fn ($value) => $value['env_variable'], $parsed['variables'] ?? []);
|
||||
|
||||
$egg->variables()->whereNotIn('env_variable', $imported)->delete();
|
||||
|
||||
return $egg->refresh();
|
||||
@ -74,31 +75,39 @@ class EggImporterService
|
||||
}
|
||||
|
||||
/**
|
||||
* Take an url and parse it into a new egg or update an existing one.
|
||||
* Take a URL (YAML or JSON) and parse it into a new egg or update an existing one.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException|\Throwable
|
||||
* @throws InvalidFileUploadException|Throwable
|
||||
*/
|
||||
public function fromUrl(string $url, ?Egg $egg = null): Egg
|
||||
{
|
||||
$info = pathinfo($url);
|
||||
$extension = strtolower($info['extension']);
|
||||
|
||||
$tmpDir = TemporaryDirectory::make()->deleteWhenDestroyed();
|
||||
$tmpPath = $tmpDir->path($info['basename']);
|
||||
|
||||
if (!file_put_contents($tmpPath, file_get_contents($url))) {
|
||||
throw new InvalidFileUploadException('Could not write temporary file.');
|
||||
$fileContents = @file_get_contents($url);
|
||||
|
||||
if (!$fileContents || !file_put_contents($tmpPath, $fileContents)) {
|
||||
throw new InvalidFileUploadException('Could not download or write temporary file.');
|
||||
}
|
||||
|
||||
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], 'application/json'), $egg);
|
||||
$mime = match ($extension) {
|
||||
'yaml', 'yml' => 'application/yaml',
|
||||
'json' => 'application/json',
|
||||
default => throw new InvalidFileUploadException('Unsupported file format.'),
|
||||
};
|
||||
|
||||
return $this->fromFile(new UploadedFile($tmpPath, $info['basename'], $mime), $egg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an uploaded file and parses out the egg configuration from within.
|
||||
*
|
||||
* @todo replace with DTO
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*
|
||||
* @throws \App\Exceptions\Service\InvalidFileUploadException
|
||||
* @throws InvalidFileUploadException|JsonException
|
||||
*/
|
||||
protected function parseFile(UploadedFile $file): array
|
||||
{
|
||||
@ -106,30 +115,56 @@ class EggImporterService
|
||||
throw new InvalidFileUploadException('The selected file was not uploaded successfully');
|
||||
}
|
||||
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
$mime = $file->getMimeType();
|
||||
|
||||
try {
|
||||
$parsed = json_decode($file->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $exception) {
|
||||
throw new InvalidFileUploadException('Could not read JSON file: ' . $exception->getMessage());
|
||||
$content = $file->getContent();
|
||||
|
||||
$parsed = match (true) {
|
||||
in_array($extension, ['yaml', 'yml']),
|
||||
str_contains($mime, 'yaml') => Yaml::parse($content),
|
||||
default => json_decode($content, true, 512, JSON_THROW_ON_ERROR),
|
||||
};
|
||||
} catch (Throwable $e) {
|
||||
throw new InvalidFileUploadException('File parse failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$version = $parsed['meta']['version'] ?? '';
|
||||
|
||||
$parsed = match ($version) {
|
||||
'PTDL_v1' => $this->convertToV2($parsed),
|
||||
'PTDL_v2' => $parsed,
|
||||
'PLCN_v1' => $parsed,
|
||||
default => throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.')
|
||||
'PTDL_v2', 'PLCN_v1', 'PLCN_v2' => $parsed,
|
||||
default => throw new InvalidFileUploadException('The file format is not recognized.'),
|
||||
};
|
||||
|
||||
// Make sure we only use recent variable format from now on
|
||||
if (array_get($parsed['config'], 'files')) {
|
||||
$parsed['config']['files'] = str_replace(
|
||||
array_keys(self::UPGRADE_VARIABLES),
|
||||
array_values(self::UPGRADE_VARIABLES),
|
||||
$parsed['config']['files'],
|
||||
);
|
||||
if (isset($parsed['config']) && (is_array($parsed['config']) || $parsed['config'] instanceof stdClass)) {
|
||||
$parsed['config'] = (array) $parsed['config'];
|
||||
foreach ($parsed['config'] as $key => $value) {
|
||||
if (is_array($value) || $value instanceof stdClass) {
|
||||
$parsed['config'][$key] = json_encode((array) $value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
if ($key === 'files' && is_string($parsed['config'][$key])) {
|
||||
$parsed['config'][$key] = str_replace(
|
||||
array_keys(self::UPGRADE_VARIABLES),
|
||||
array_values(self::UPGRADE_VARIABLES),
|
||||
$parsed['config'][$key]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parsed['scripts']['installation']) && (is_array($parsed['scripts']['installation']) || $parsed['scripts']['installation'] instanceof stdClass)) {
|
||||
$parsed['scripts']['installation'] = (array) $parsed['scripts']['installation'];
|
||||
foreach ($parsed['scripts']['installation'] as $key => $value) {
|
||||
if (is_array($value) || $value instanceof stdClass) {
|
||||
$parsed['scripts']['installation'][$key] = json_encode((array) $value, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reserved env var name handling
|
||||
[$forbidden, $allowed] = collect($parsed['variables'])
|
||||
->map(fn ($variable) => array_merge(
|
||||
$variable,
|
||||
@ -155,22 +190,7 @@ class EggImporterService
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the provided model with the parsed JSON data.
|
||||
*
|
||||
* @param array{
|
||||
* name: string,
|
||||
* description: string,
|
||||
* tags: string[],
|
||||
* features: string[],
|
||||
* docker_images: string[],
|
||||
* file_denylist: string[],
|
||||
* meta: array{update_url: string},
|
||||
* config: array{files: string, startup: string, logs: string, stop: string},
|
||||
* startup: string,
|
||||
* scripts: array{
|
||||
* installation: array{script: string, entrypoint: string, container: string},
|
||||
* },
|
||||
* } $parsed
|
||||
* @param array<string, mixed> $parsed
|
||||
*/
|
||||
protected function fillFromParsed(Egg $model, array $parsed): Egg
|
||||
{
|
||||
@ -182,9 +202,9 @@ class EggImporterService
|
||||
'docker_images' => Arr::get($parsed, 'docker_images'),
|
||||
'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist'))->filter(fn ($value) => !empty($value)),
|
||||
'update_url' => Arr::get($parsed, 'meta.update_url'),
|
||||
'config_files' => Arr::get($parsed, 'config.files'),
|
||||
'config_startup' => Arr::get($parsed, 'config.startup'),
|
||||
'config_logs' => Arr::get($parsed, 'config.logs'),
|
||||
'config_files' => json_encode(json_decode(Arr::get($parsed, 'config.files')), JSON_PRETTY_PRINT),
|
||||
'config_startup' => json_encode(json_decode(Arr::get($parsed, 'config.startup')), JSON_PRETTY_PRINT),
|
||||
'config_logs' => json_encode(json_decode(Arr::get($parsed, 'config.logs')), JSON_PRETTY_PRINT),
|
||||
'config_stop' => Arr::get($parsed, 'config.stop'),
|
||||
'startup' => Arr::get($parsed, 'startup'),
|
||||
'script_install' => Arr::get($parsed, 'scripts.installation.script'),
|
||||
@ -194,17 +214,11 @@ class EggImporterService
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles
|
||||
* the "docker_images" field potentially not being present, and not being in the
|
||||
* expected "key => value" format.
|
||||
*
|
||||
* @param array{images?: string[], image?: string, field_type?: string, docker_images?: array<array-key, string>} $parsed
|
||||
* @return array<array-key, mixed>
|
||||
* @param array<string, mixed> $parsed
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function convertToV2(array $parsed): array
|
||||
{
|
||||
// Maintain backwards compatability for eggs that are still using the old single image
|
||||
// string format. New eggs can provide an array of Docker images that can be used.
|
||||
if (!isset($parsed['images'])) {
|
||||
$images = [Arr::get($parsed, 'image') ?? 'nil'];
|
||||
} else {
|
||||
|
@ -2,16 +2,16 @@
|
||||
|
||||
namespace App\Services\Nodes;
|
||||
|
||||
use App\Extensions\Lcobucci\JWT\Encoding\TimestampDates;
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTimeImmutable;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\Node;
|
||||
use App\Models\User;
|
||||
use Lcobucci\JWT\Token\Plain;
|
||||
use Lcobucci\JWT\Configuration;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||
use App\Extensions\Lcobucci\JWT\Encoding\TimestampDates;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
|
||||
class NodeJWTService
|
||||
{
|
||||
@ -64,7 +64,7 @@ class NodeJWTService
|
||||
/**
|
||||
* Generate a new JWT for a given node.
|
||||
*/
|
||||
public function handle(Node $node, ?string $identifiedBy, string $algo = 'md5'): Plain
|
||||
public function handle(Node $node, ?string $identifiedBy, string $algo = 'sha256'): UnencryptedToken
|
||||
{
|
||||
$identifier = hash($algo, $identifiedBy);
|
||||
$config = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($node->daemon_token));
|
||||
@ -80,7 +80,9 @@ class NodeJWTService
|
||||
$builder = $builder->expiresAt($this->expiresAt);
|
||||
|
||||
if (!empty($this->subject)) {
|
||||
$builder = $builder->relatedTo($this->subject)->withHeader('sub', $this->subject);
|
||||
$builder = $builder
|
||||
->relatedTo($this->subject)
|
||||
->withHeader('sub', $this->subject);
|
||||
}
|
||||
|
||||
foreach ($this->claims as $key => $value) {
|
||||
@ -88,14 +90,7 @@ class NodeJWTService
|
||||
}
|
||||
|
||||
if (!is_null($this->user)) {
|
||||
$builder = $builder
|
||||
->withClaim('user_uuid', $this->user->uuid)
|
||||
// The "user_id" claim is deprecated and should not be referenced — it remains
|
||||
// here solely to ensure older versions of daemon are unaffected when the Panel
|
||||
// is updated.
|
||||
//
|
||||
// This claim will be removed in Panel@1.11 or later.
|
||||
->withClaim('user_id', $this->user->id);
|
||||
$builder = $builder->withClaim('user_uuid', $this->user->uuid);
|
||||
}
|
||||
|
||||
return $builder
|
||||
|
@ -39,15 +39,7 @@ class ServerCreationService
|
||||
* as possible given the input data. For example, if an allocation_id is passed with
|
||||
* no node_id the node_is will be picked from the allocation.
|
||||
*
|
||||
* @param array{
|
||||
* node_id?: int,
|
||||
* oom_killer?: bool,
|
||||
* oom_disabled?: bool,
|
||||
* egg_id?: int,
|
||||
* image?: ?string,
|
||||
* startup?: ?string,
|
||||
* start_on_completion?: ?bool,
|
||||
* } $data
|
||||
* @param array<mixed, mixed> $data
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
@ -64,8 +56,8 @@ class ServerCreationService
|
||||
$egg = Egg::query()->findOrFail($data['egg_id']);
|
||||
|
||||
// Fill missing fields from egg
|
||||
$data['image'] = $data['image'] ?? collect($egg->docker_images)->first();
|
||||
$data['startup'] = $data['startup'] ?? $egg->startup;
|
||||
$data['image'] ??= collect($egg->docker_images)->first();
|
||||
$data['startup'] ??= $egg->startup;
|
||||
|
||||
// If a deployment object has been passed we need to get the allocation and node that the server should use.
|
||||
if ($deployment) {
|
||||
@ -94,6 +86,8 @@ class ServerCreationService
|
||||
if (empty($data['node_id'])) {
|
||||
$data['node_id'] = $nodes->first();
|
||||
}
|
||||
} else {
|
||||
$data['node_id'] ??= Allocation::find($data['allocation_id'])?->node_id;
|
||||
}
|
||||
|
||||
Assert::false(empty($data['node_id']), 'Expected a non-empty node_id in server creation data.');
|
||||
|
@ -10,7 +10,7 @@ use App\Services\Nodes\NodeJWTService;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Lcobucci\JWT\Token\Plain;
|
||||
use Lcobucci\JWT\UnencryptedToken;
|
||||
|
||||
class TransferServerService
|
||||
{
|
||||
@ -22,7 +22,7 @@ class TransferServerService
|
||||
private NodeJWTService $nodeJWTService,
|
||||
) {}
|
||||
|
||||
private function notify(ServerTransfer $transfer, Plain $token): void
|
||||
private function notify(ServerTransfer $transfer, UnencryptedToken $token): void
|
||||
{
|
||||
Http::daemon($transfer->oldNode)->post("/api/servers/{$transfer->server->uuid}/transfer", [
|
||||
'url' => $transfer->newNode->getConnectionAddress() . '/api/transfers',
|
||||
|
@ -4,7 +4,6 @@ namespace App\Services\Subusers;
|
||||
|
||||
use App\Events\Server\SubUserAdded;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\Server;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
@ -40,14 +39,8 @@ class SubuserCreationService
|
||||
return $this->connection->transaction(function () use ($server, $email, $permissions) {
|
||||
$user = User::query()->where('email', $email)->first();
|
||||
if (!$user) {
|
||||
// Just cap the username generated at 64 characters at most and then append a random string
|
||||
// to the end to make it "unique"...
|
||||
[$beforeDomain] = explode('@', $email, 1);
|
||||
$username = substr(preg_replace('/([^\w.-]+)/', '', $beforeDomain), 0, 64) . Str::random(3);
|
||||
|
||||
$user = $this->userCreationService->handle([
|
||||
'email' => $email,
|
||||
'username' => $username,
|
||||
'root_admin' => false,
|
||||
]);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Users;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Support\Str;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Hashing\Hasher;
|
||||
@ -42,6 +43,16 @@ class UserCreationService
|
||||
$isRootAdmin = array_key_exists('root_admin', $data) && $data['root_admin'];
|
||||
unset($data['root_admin']);
|
||||
|
||||
if (empty($data['username'])) {
|
||||
$data['username'] = str($data['email'])->before('@')->toString() . Str::random(3);
|
||||
}
|
||||
|
||||
$data['username'] = str($data['username'])
|
||||
->replace(['.', '-'], '')
|
||||
->ascii()
|
||||
->substr(0, 64)
|
||||
->toString();
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::query()->forceCreate(array_merge($data, [
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
|
@ -45,6 +45,7 @@ services:
|
||||
<<: [*panel-environment, *mail-environment]
|
||||
XDG_DATA_HOME: /pelican-data
|
||||
# SKIP_CADDY: true # enable when not using caddy.
|
||||
TRUSTED_PROXIES:
|
||||
|
||||
volumes:
|
||||
pelican-data:
|
||||
|
@ -17,13 +17,13 @@
|
||||
"doctrine/dbal": "~3.6.0",
|
||||
"filament/filament": "^3.3",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"laravel/framework": "^12.21",
|
||||
"laravel/framework": "^12.23",
|
||||
"laravel/helpers": "^1.7",
|
||||
"laravel/sanctum": "^4.1",
|
||||
"laravel/socialite": "^5.21",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/ui": "^4.6",
|
||||
"lcobucci/jwt": "~4.3.0",
|
||||
"lcobucci/jwt": "^5.5",
|
||||
"league/flysystem-aws-s3-v3": "^3.29",
|
||||
"league/flysystem-memory": "^3.29",
|
||||
"phpseclib/phpseclib": "~3.0.18",
|
||||
@ -77,18 +77,13 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"cs:fix": "php-cs-fixer fix",
|
||||
"cs:check": "php-cs-fixer fix --dry-run --diff --verbose",
|
||||
"pint": "pint",
|
||||
"phpstan": "phpstan --memory-limit=-1",
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump"
|
||||
],
|
||||
"post-install-cmd": [
|
||||
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
|
823
composer.lock
generated
823
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -50,7 +50,6 @@ return [
|
||||
],
|
||||
|
||||
'filament' => [
|
||||
'top-navigation' => env('FILAMENT_TOP_NAVIGATION', false),
|
||||
'display-width' => env('FILAMENT_WIDTH', 'screen-2xl'),
|
||||
'avatar-provider' => env('FILAMENT_AVATAR_PROVIDER', 'gravatar'),
|
||||
'uploadable-avatars' => env('FILAMENT_UPLOADABLE_AVATARS', false),
|
||||
|
@ -3,10 +3,12 @@
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Egg;
|
||||
use Exception;
|
||||
use DirectoryIterator;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
|
||||
class EggSeeder extends Seeder
|
||||
{
|
||||
@ -46,22 +48,39 @@ class EggSeeder extends Seeder
|
||||
*/
|
||||
protected function parseEggFiles($name): void
|
||||
{
|
||||
$files = new \DirectoryIterator(database_path('Seeders/eggs/' . kebab_case($name)));
|
||||
$path = database_path('Seeders/eggs/' . kebab_case($name));
|
||||
$files = new DirectoryIterator($path);
|
||||
|
||||
$this->command->alert('Updating Eggs for: ' . $name);
|
||||
/** @var \DirectoryIterator $file */
|
||||
|
||||
/** @var DirectoryIterator $file */
|
||||
foreach ($files as $file) {
|
||||
if (!$file->isFile() || !$file->isReadable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extension = strtolower($file->getExtension());
|
||||
$filePath = $file->getRealPath();
|
||||
|
||||
try {
|
||||
$decoded = json_decode(file_get_contents($file->getRealPath()), true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Exception) {
|
||||
$decoded = match ($extension) {
|
||||
'json' => json_decode(file_get_contents($filePath), true, 512, JSON_THROW_ON_ERROR),
|
||||
'yaml', 'yml' => Yaml::parseFile($filePath),
|
||||
default => null,
|
||||
};
|
||||
} catch (Throwable) {
|
||||
$this->command->warn("Failed to parse {$file->getFilename()}, skipping.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json');
|
||||
if (!is_array($decoded) || !isset($decoded['name'], $decoded['author'])) {
|
||||
$this->command->warn("Invalid structure in {$file->getFilename()}, skipping.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$uploaded = new UploadedFile($filePath, $file->getFilename());
|
||||
|
||||
$egg = Egg::query()
|
||||
->where('author', $decoded['author'])
|
||||
@ -69,10 +88,10 @@ class EggSeeder extends Seeder
|
||||
->first();
|
||||
|
||||
if ($egg instanceof Egg) {
|
||||
$this->importerService->fromFile($file, $egg);
|
||||
$this->importerService->fromFile($uploaded, $egg);
|
||||
$this->command->info('Updated ' . $decoded['name']);
|
||||
} else {
|
||||
$this->importerService->fromFile($file);
|
||||
$this->importerService->fromFile($uploaded);
|
||||
$this->command->comment('Created ' . $decoded['name']);
|
||||
}
|
||||
}
|
||||
|
@ -1,72 +0,0 @@
|
||||
{
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PLCN_v1",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-bungeecord.json"
|
||||
},
|
||||
"exported_at": "2025-03-18T12:35:34+00:00",
|
||||
"name": "Bungeecord",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "9e6b409e-4028-4947-aea8-50a2c404c271",
|
||||
"description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.",
|
||||
"tags": [
|
||||
"minecraft",
|
||||
"proxy"
|
||||
],
|
||||
"features": [
|
||||
"eula",
|
||||
"java_version",
|
||||
"pid_limit"
|
||||
],
|
||||
"docker_images": {
|
||||
"Java 21": "ghcr.io\/parkervcp\/yolks:java_21",
|
||||
"Java 17": "ghcr.io\/parkervcp\/yolks:java_17",
|
||||
"Java 16": "ghcr.io\/parkervcp\/yolks:java_16",
|
||||
"Java 11": "ghcr.io\/parkervcp\/yolks:java_11",
|
||||
"Java 8": "ghcr.io\/parkervcp\/yolks:java_8"
|
||||
},
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"config.yml\": {\r\n \"parser\": \"yaml\",\r\n \"find\": {\r\n \"listeners[0].query_port\": \"{{server.allocations.default.port}}\",\r\n \"listeners[0].host\": \"0.0.0.0:{{server.allocations.default.port}}\",\r\n \"servers.*.address\": {\r\n \"regex:^(127\\\\.0\\\\.0\\\\.1|localhost)(:\\\\d{1,5})?$\": \"{{config.docker.interface}}$2\"\r\n }\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \"Listening on \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "end"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/ash\r\n# Bungeecord Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\nif [ -z \"${BUNGEE_VERSION}\" ] || [ \"${BUNGEE_VERSION}\" == \"latest\" ]; then\r\n BUNGEE_VERSION=\"lastStableBuild\"\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} https:\/\/ci.md-5.net\/job\/BungeeCord\/${BUNGEE_VERSION}\/artifact\/bootstrap\/target\/BungeeCord.jar",
|
||||
"container": "ghcr.io\/parkervcp\/installers:alpine",
|
||||
"entrypoint": "ash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"name": "Bungeecord Version",
|
||||
"description": "The version of Bungeecord to download and use.",
|
||||
"env_variable": "BUNGEE_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"alpha_num",
|
||||
"between:1,6"
|
||||
],
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "Bungeecord Jar File",
|
||||
"description": "The name of the Jarfile to use when running Bungeecord.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "bungeecord.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
],
|
||||
"sort": 2
|
||||
}
|
||||
]
|
||||
}
|
83
database/Seeders/eggs/minecraft/egg-bungeecord.yaml
Normal file
83
database/Seeders/eggs/minecraft/egg-bungeecord.yaml
Normal file
@ -0,0 +1,83 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v2
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-bungeecord.yaml'
|
||||
exported_at: '2025-07-25T13:32:34+00:00'
|
||||
name: Bungeecord
|
||||
author: panel@example.com
|
||||
uuid: 9e6b409e-4028-4947-aea8-50a2c404c271
|
||||
description: |-
|
||||
For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and
|
||||
reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream.
|
||||
Whether you are a small server wishing to string multiple game-modes together, or the owner of the
|
||||
ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be
|
||||
able to unlock your community's full potential.
|
||||
tags:
|
||||
- minecraft
|
||||
- proxy
|
||||
features:
|
||||
- eula
|
||||
- java_version
|
||||
- pid_limit
|
||||
docker_images:
|
||||
'Java 21': 'ghcr.io/parkervcp/yolks:java_21'
|
||||
'Java 17': 'ghcr.io/parkervcp/yolks:java_17'
|
||||
'Java 16': 'ghcr.io/parkervcp/yolks:java_16'
|
||||
'Java 11': 'ghcr.io/parkervcp/yolks:java_11'
|
||||
'Java 8': 'ghcr.io/parkervcp/yolks:java_8'
|
||||
file_denylist: { }
|
||||
startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
|
||||
config:
|
||||
files:
|
||||
config.yml:
|
||||
parser: yaml
|
||||
find:
|
||||
'listeners[0].query_port': '{{server.allocations.default.port}}'
|
||||
'listeners[0].host': '0.0.0.0:{{server.allocations.default.port}}'
|
||||
'servers.*.address':
|
||||
'regex:^(127\.0\.0\.1|localhost)(:\d{1,5})?$': '{{config.docker.interface}}$2'
|
||||
startup:
|
||||
done: 'Listening on '
|
||||
logs: { }
|
||||
stop: end
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/ash
|
||||
# Bungeecord Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
|
||||
cd /mnt/server
|
||||
|
||||
if [ -z "${BUNGEE_VERSION}" ] || [ "${BUNGEE_VERSION}" == "latest" ]; then
|
||||
BUNGEE_VERSION="lastStableBuild"
|
||||
fi
|
||||
|
||||
curl -o ${SERVER_JARFILE} https://ci.md-5.net/job/BungeeCord/${BUNGEE_VERSION}/artifact/bootstrap/target/BungeeCord.jar
|
||||
container: 'ghcr.io/parkervcp/installers:alpine'
|
||||
entrypoint: ash
|
||||
variables:
|
||||
-
|
||||
name: 'Bungeecord Jar File'
|
||||
description: 'The name of the Jarfile to use when running Bungeecord.'
|
||||
env_variable: SERVER_JARFILE
|
||||
default_value: bungeecord.jar
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'regex:/^([\w\d._-]+)(\.jar)$/'
|
||||
sort: 2
|
||||
-
|
||||
name: 'Bungeecord Version'
|
||||
description: 'The version of Bungeecord to download and use.'
|
||||
env_variable: BUNGEE_VERSION
|
||||
default_value: latest
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- alpha_num
|
||||
- 'between:1,6'
|
||||
sort: 1
|
File diff suppressed because one or more lines are too long
217
database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml
Normal file
217
database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml
Normal file
@ -0,0 +1,217 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v2
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-forge-minecraft.yaml'
|
||||
exported_at: '2025-08-05T21:00:17+00:00'
|
||||
name: 'Forge Minecraft'
|
||||
author: panel@example.com
|
||||
uuid: ed072427-f209-4603-875c-f540c6dd5a65
|
||||
description: |-
|
||||
Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which
|
||||
makes it easier to create mods, and also make sure mods are compatible with each other.
|
||||
tags:
|
||||
- minecraft
|
||||
features:
|
||||
- eula
|
||||
- java_version
|
||||
- pid_limit
|
||||
docker_images:
|
||||
'Java 21': 'ghcr.io/parkervcp/yolks:java_21'
|
||||
'Java 17': 'ghcr.io/parkervcp/yolks:java_17'
|
||||
'Java 16': 'ghcr.io/parkervcp/yolks:java_16'
|
||||
'Java 11': 'ghcr.io/parkervcp/yolks:java_11'
|
||||
'Java 8': 'ghcr.io/parkervcp/yolks:java_8'
|
||||
file_denylist: { }
|
||||
startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s "-jar {{SERVER_JARFILE}}" || printf %s "@unix_args.txt" )'
|
||||
config:
|
||||
files:
|
||||
server.properties:
|
||||
parser: properties
|
||||
find:
|
||||
server-ip: ''
|
||||
server-port: '{{server.allocations.default.port}}'
|
||||
query.port: '{{server.allocations.default.port}}'
|
||||
startup:
|
||||
done: ')! For help, type '
|
||||
logs: { }
|
||||
stop: stop
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/bash
|
||||
# Forge Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
apt update
|
||||
apt install -y curl jq
|
||||
|
||||
if [[ ! -d /mnt/server ]]; then
|
||||
mkdir /mnt/server
|
||||
fi
|
||||
|
||||
cd /mnt/server
|
||||
|
||||
# Remove spaces from the version number to avoid issues with curl
|
||||
FORGE_VERSION="$(echo "$FORGE_VERSION" | tr -d ' ')"
|
||||
MC_VERSION="$(echo "$MC_VERSION" | tr -d ' ')"
|
||||
|
||||
if [[ ! -z ${FORGE_VERSION} ]]; then
|
||||
DOWNLOAD_LINK=https://maven.minecraftforge.net/net/minecraftforge/forge/${FORGE_VERSION}/forge-${FORGE_VERSION}
|
||||
FORGE_JAR=forge-${FORGE_VERSION}*.jar
|
||||
else
|
||||
JSON_DATA=$(curl -sSL https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json)
|
||||
|
||||
if [[ "${MC_VERSION}" == "latest" ]] || [[ "${MC_VERSION}" == "" ]]; then
|
||||
echo -e "getting latest version of forge."
|
||||
MC_VERSION=$(echo -e ${JSON_DATA} | jq -r '.promos | del(."latest-1.7.10") | del(."1.7.10-latest-1.7.10") | to_entries[] | .key | select(contains("latest")) | split("-")[0]' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | tail -1)
|
||||
BUILD_TYPE=latest
|
||||
fi
|
||||
|
||||
if [[ "${BUILD_TYPE}" != "recommended" ]] && [[ "${BUILD_TYPE}" != "latest" ]]; then
|
||||
BUILD_TYPE=recommended
|
||||
fi
|
||||
|
||||
echo -e "minecraft version: ${MC_VERSION}"
|
||||
echo -e "build type: ${BUILD_TYPE}"
|
||||
|
||||
## some variables for getting versions and things
|
||||
FILE_SITE=https://maven.minecraftforge.net/net/minecraftforge/forge/
|
||||
VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION "${MC_VERSION}" --arg BUILD_TYPE "${BUILD_TYPE}" '.promos | del(."latest-1.7.10") | del(."1.7.10-latest-1.7.10") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains($BUILD_TYPE))')
|
||||
|
||||
## locating the forge version
|
||||
if [[ "${VERSION_KEY}" == "" ]] && [[ "${BUILD_TYPE}" == "recommended" ]]; then
|
||||
echo -e "dropping back to latest from recommended due to there not being a recommended version of forge for the mc version requested."
|
||||
VERSION_KEY=$(echo -e ${JSON_DATA} | jq -r --arg MC_VERSION "${MC_VERSION}" '.promos | del(."latest-1.7.10") | del(."1.7.10-latest-1.7.10") | to_entries[] | .key | select(contains($MC_VERSION)) | select(contains("latest"))')
|
||||
fi
|
||||
|
||||
## Error if the mc version set wasn't valid.
|
||||
if [ "${VERSION_KEY}" == "" ] || [ "${VERSION_KEY}" == "null" ]; then
|
||||
echo -e "The install failed because there is no valid version of forge for the version of minecraft selected."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FORGE_VERSION=$(echo -e ${JSON_DATA} | jq -r --arg VERSION_KEY "$VERSION_KEY" '.promos | .[$VERSION_KEY]')
|
||||
|
||||
if [[ "${MC_VERSION}" == "1.7.10" ]] || [[ "${MC_VERSION}" == "1.8.9" ]]; then
|
||||
DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}/forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}
|
||||
FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}.jar
|
||||
if [[ "${MC_VERSION}" == "1.7.10" ]]; then
|
||||
FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}-${MC_VERSION}-universal.jar
|
||||
fi
|
||||
else
|
||||
DOWNLOAD_LINK=${FILE_SITE}${MC_VERSION}-${FORGE_VERSION}/forge-${MC_VERSION}-${FORGE_VERSION}
|
||||
FORGE_JAR=forge-${MC_VERSION}-${FORGE_VERSION}.jar
|
||||
fi
|
||||
fi
|
||||
|
||||
#Adding .jar when not eding by SERVER_JARFILE
|
||||
if [[ ! $SERVER_JARFILE = *\.jar ]]; then
|
||||
SERVER_JARFILE="$SERVER_JARFILE.jar"
|
||||
fi
|
||||
|
||||
#Downloading jars
|
||||
echo -e "Downloading forge version ${FORGE_VERSION}"
|
||||
echo -e "Download link is ${DOWNLOAD_LINK}"
|
||||
|
||||
if [[ ! -z "${DOWNLOAD_LINK}" ]]; then
|
||||
if curl --output /dev/null --silent --head --fail ${DOWNLOAD_LINK}-installer.jar; then
|
||||
echo -e "installer jar download link is valid."
|
||||
else
|
||||
echo -e "link is invalid. Exiting now"
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
echo -e "no download link provided. Exiting now"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
curl -s -o installer.jar -sS ${DOWNLOAD_LINK}-installer.jar
|
||||
|
||||
#Checking if downloaded jars exist
|
||||
if [[ ! -f ./installer.jar ]]; then
|
||||
echo "!!! Error downloading forge version ${FORGE_VERSION} !!!"
|
||||
exit
|
||||
fi
|
||||
|
||||
function unix_args {
|
||||
echo -e "Detected Forge 1.17 or newer version. Setting up forge unix args."
|
||||
ln -sf libraries/net/minecraftforge/forge/*/unix_args.txt unix_args.txt
|
||||
}
|
||||
|
||||
# Delete args to support downgrading/upgrading
|
||||
rm -rf libraries/net/minecraftforge/forge
|
||||
rm unix_args.txt
|
||||
|
||||
#Installing server
|
||||
echo -e "Installing forge server.
|
||||
"
|
||||
java -jar installer.jar --installServer || { echo -e "
|
||||
Install failed using Forge version ${FORGE_VERSION} and Minecraft version ${MINECRAFT_VERSION}.
|
||||
Should you be using unlimited memory value of 0, make sure to increase the default install resource limits in the Daemon config or specify exact allocated memory in the server Build Configuration instead of 0!
|
||||
Otherwise, the Forge installer will not have enough memory."; exit 4; }
|
||||
|
||||
# Check if we need a symlink for 1.17+ Forge JPMS args
|
||||
if [[ $MC_VERSION =~ ^1\.(17|18|19|20|21|22|23) || $FORGE_VERSION =~ ^1\.(17|18|19|20|21|22|23) ]]; then
|
||||
unix_args
|
||||
|
||||
# Check if someone has set MC to latest but overwrote it with older Forge version, otherwise we would have false positives
|
||||
elif [[ $MC_VERSION == "latest" && $FORGE_VERSION =~ ^1\.(17|18|19|20|21|22|23) ]]; then
|
||||
unix_args
|
||||
else
|
||||
# For versions below 1.17 that ship with jar
|
||||
mv $FORGE_JAR $SERVER_JARFILE
|
||||
fi
|
||||
|
||||
echo -e "Deleting installer.jar file.
|
||||
"
|
||||
rm -rf installer.jar
|
||||
echo -e "Installation process is completed"
|
||||
container: 'openjdk:8-jdk-slim'
|
||||
entrypoint: bash
|
||||
variables:
|
||||
-
|
||||
name: 'Build Type'
|
||||
description: "The type of server jar to download from forge.\r\n\r\nValid types are \"recommended\" and \"latest\"."
|
||||
env_variable: BUILD_TYPE
|
||||
default_value: recommended
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- 'in:recommended,latest'
|
||||
sort: 3
|
||||
-
|
||||
name: 'Forge Version'
|
||||
description: "The full exact version.\r\n\r\nEx. 1.15.2-31.2.4\r\n\r\nOverrides MC_VERSION and BUILD_TYPE. If it fails to download the server files it will fail to\ninstall."
|
||||
env_variable: FORGE_VERSION
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- nullable
|
||||
- 'regex:/^[0-9\.\-]+$/'
|
||||
sort: 4
|
||||
-
|
||||
name: 'Minecraft Version'
|
||||
description: "The version of minecraft you want to install for.\r\n\r\nLeaving latest will install the latest recommended version."
|
||||
env_variable: MC_VERSION
|
||||
default_value: latest
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- 'max:9'
|
||||
sort: 2
|
||||
-
|
||||
name: 'Server Jar File'
|
||||
description: 'The name of the Jarfile to use when running Forge version below 1.17.'
|
||||
env_variable: SERVER_JARFILE
|
||||
default_value: server.jar
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'regex:/^([\w\d._-]+)(\.jar)$/'
|
||||
sort: 1
|
@ -1,98 +0,0 @@
|
||||
{
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PLCN_v1",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-paper.json"
|
||||
},
|
||||
"exported_at": "2025-03-18T12:35:44+00:00",
|
||||
"name": "Paper",
|
||||
"author": "parker@example.com",
|
||||
"uuid": "5da37ef6-58da-4169-90a6-e683e1721247",
|
||||
"description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.",
|
||||
"tags": [
|
||||
"minecraft"
|
||||
],
|
||||
"features": [
|
||||
"eula",
|
||||
"java_version",
|
||||
"pid_limit"
|
||||
],
|
||||
"docker_images": {
|
||||
"Java 21": "ghcr.io\/parkervcp\/yolks:java_21",
|
||||
"Java 17": "ghcr.io\/parkervcp\/yolks:java_17",
|
||||
"Java 16": "ghcr.io\/parkervcp\/yolks:java_16",
|
||||
"Java 11": "ghcr.io\/parkervcp\/yolks:java_11",
|
||||
"Java 8": "ghcr.io\/parkervcp\/yolks:java_8"
|
||||
},
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/ash\r\n# Paper Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\nPROJECT=paper\r\n\r\nif [ -n \"${DL_PATH}\" ]; then\r\n\techo -e \"Using supplied download url: ${DL_PATH}\"\r\n\tDOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's\/{{\/${\/g' -e 's\/}}\/}\/g')`\r\nelse\r\n\tVER_EXISTS=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT} | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep -m1 true`\r\n\tLATEST_VERSION=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT} | jq -r '.versions' | jq -r '.[-1]'`\r\n\r\n\tif [ \"${VER_EXISTS}\" == \"true\" ]; then\r\n\t\techo -e \"Version is valid. Using version ${MINECRAFT_VERSION}\"\r\n\telse\r\n\t\techo -e \"Specified version not found. Defaulting to the latest ${PROJECT} version\"\r\n\t\tMINECRAFT_VERSION=${LATEST_VERSION}\r\n\tfi\r\n\r\n\tBUILD_EXISTS=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds[] | tostring | contains($BUILD)' | grep -m1 true`\r\n\tLATEST_BUILD=`curl -s https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION} | jq -r '.builds' | jq -r '.[-1]'`\r\n\r\n\tif [ \"${BUILD_EXISTS}\" == \"true\" ]; then\r\n\t\techo -e \"Build is valid for version ${MINECRAFT_VERSION}. Using build ${BUILD_NUMBER}\"\r\n\telse\r\n\t\techo -e \"Using the latest ${PROJECT} build for version ${MINECRAFT_VERSION}\"\r\n\t\tBUILD_NUMBER=${LATEST_BUILD}\r\n\tfi\r\n\r\n\tJAR_NAME=${PROJECT}-${MINECRAFT_VERSION}-${BUILD_NUMBER}.jar\r\n\r\n\techo \"Version being downloaded\"\r\n\techo -e \"MC Version: ${MINECRAFT_VERSION}\"\r\n\techo -e \"Build: ${BUILD_NUMBER}\"\r\n\techo -e \"JAR Name of Build: ${JAR_NAME}\"\r\n\tDOWNLOAD_URL=https:\/\/api.papermc.io\/v2\/projects\/${PROJECT}\/versions\/${MINECRAFT_VERSION}\/builds\/${BUILD_NUMBER}\/downloads\/${JAR_NAME}\r\nfi\r\n\r\ncd \/mnt\/server\r\n\r\necho -e \"Running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\"\r\n\r\nif [ -f ${SERVER_JARFILE} ]; then\r\n\tmv ${SERVER_JARFILE} ${SERVER_JARFILE}.old\r\nfi\r\n\r\ncurl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}\r\n\r\nif [ ! -f server.properties ]; then\r\n echo -e \"Downloading MC server.properties\"\r\n curl -o server.properties https:\/\/raw.githubusercontent.com\/parkervcp\/eggs\/master\/minecraft\/java\/server.properties\r\nfi",
|
||||
"container": "ghcr.io\/parkervcp\/installers:alpine",
|
||||
"entrypoint": "ash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"name": "Minecraft Version",
|
||||
"description": "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.",
|
||||
"env_variable": "MINECRAFT_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string",
|
||||
"max:20"
|
||||
],
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "Server Jar File",
|
||||
"description": "The name of the server jarfile to run the server with.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
],
|
||||
"sort": 2
|
||||
},
|
||||
{
|
||||
"name": "Download Path",
|
||||
"description": "A URL to use to download a server.jar rather than the ones in the install script. This is not user viewable.",
|
||||
"env_variable": "DL_PATH",
|
||||
"default_value": "",
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string"
|
||||
],
|
||||
"sort": 3
|
||||
},
|
||||
{
|
||||
"name": "Build Number",
|
||||
"description": "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest.",
|
||||
"env_variable": "BUILD_NUMBER",
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"max:20"
|
||||
],
|
||||
"sort": 4
|
||||
}
|
||||
]
|
||||
}
|
142
database/Seeders/eggs/minecraft/egg-paper.yaml
Normal file
142
database/Seeders/eggs/minecraft/egg-paper.yaml
Normal file
@ -0,0 +1,142 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v2
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-paper.yaml'
|
||||
exported_at: '2025-08-05T21:00:17+00:00'
|
||||
name: Paper
|
||||
author: parker@example.com
|
||||
uuid: 5da37ef6-58da-4169-90a6-e683e1721247
|
||||
description: 'High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.'
|
||||
tags:
|
||||
- minecraft
|
||||
features:
|
||||
- eula
|
||||
- java_version
|
||||
- pid_limit
|
||||
docker_images:
|
||||
'Java 21': 'ghcr.io/parkervcp/yolks:java_21'
|
||||
'Java 17': 'ghcr.io/parkervcp/yolks:java_17'
|
||||
'Java 16': 'ghcr.io/parkervcp/yolks:java_16'
|
||||
'Java 11': 'ghcr.io/parkervcp/yolks:java_11'
|
||||
'Java 8': 'ghcr.io/parkervcp/yolks:java_8'
|
||||
file_denylist: { }
|
||||
startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}'
|
||||
config:
|
||||
files:
|
||||
server.properties:
|
||||
parser: properties
|
||||
find:
|
||||
server-ip: ''
|
||||
server-port: '{{server.allocations.default.port}}'
|
||||
query.port: '{{server.allocations.default.port}}'
|
||||
startup:
|
||||
done: ')! For help, type '
|
||||
logs: { }
|
||||
stop: stop
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/ash
|
||||
# Paper Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
PROJECT=paper
|
||||
|
||||
if [ -n "${DL_PATH}" ]; then
|
||||
echo -e "Using supplied download url: ${DL_PATH}"
|
||||
DOWNLOAD_URL=`eval echo $(echo ${DL_PATH} | sed -e 's/{{/${/g' -e 's/}}/}/g')`
|
||||
else
|
||||
VER_EXISTS=`curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r --arg VERSION $MINECRAFT_VERSION '.versions[] | contains($VERSION)' | grep -m1 true`
|
||||
LATEST_VERSION=`curl -s https://api.papermc.io/v2/projects/${PROJECT} | jq -r '.versions' | jq -r '.[-1]'`
|
||||
|
||||
if [ "${VER_EXISTS}" == "true" ]; then
|
||||
echo -e "Version is valid. Using version ${MINECRAFT_VERSION}"
|
||||
else
|
||||
echo -e "Specified version not found. Defaulting to the latest ${PROJECT} version"
|
||||
MINECRAFT_VERSION=${LATEST_VERSION}
|
||||
fi
|
||||
|
||||
BUILD_EXISTS=`curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r --arg BUILD ${BUILD_NUMBER} '.builds[] | tostring | contains($BUILD)' | grep -m1 true`
|
||||
LATEST_BUILD=`curl -s https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION} | jq -r '.builds' | jq -r '.[-1]'`
|
||||
|
||||
if [ "${BUILD_EXISTS}" == "true" ]; then
|
||||
echo -e "Build is valid for version ${MINECRAFT_VERSION}. Using build ${BUILD_NUMBER}"
|
||||
else
|
||||
echo -e "Using the latest ${PROJECT} build for version ${MINECRAFT_VERSION}"
|
||||
BUILD_NUMBER=${LATEST_BUILD}
|
||||
fi
|
||||
|
||||
JAR_NAME=${PROJECT}-${MINECRAFT_VERSION}-${BUILD_NUMBER}.jar
|
||||
|
||||
echo "Version being downloaded"
|
||||
echo -e "MC Version: ${MINECRAFT_VERSION}"
|
||||
echo -e "Build: ${BUILD_NUMBER}"
|
||||
echo -e "JAR Name of Build: ${JAR_NAME}"
|
||||
DOWNLOAD_URL=https://api.papermc.io/v2/projects/${PROJECT}/versions/${MINECRAFT_VERSION}/builds/${BUILD_NUMBER}/downloads/${JAR_NAME}
|
||||
fi
|
||||
|
||||
cd /mnt/server
|
||||
|
||||
echo -e "Running curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}"
|
||||
|
||||
if [ -f ${SERVER_JARFILE} ]; then
|
||||
mv ${SERVER_JARFILE} ${SERVER_JARFILE}.old
|
||||
fi
|
||||
|
||||
curl -o ${SERVER_JARFILE} ${DOWNLOAD_URL}
|
||||
|
||||
if [ ! -f server.properties ]; then
|
||||
echo -e "Downloading MC server.properties"
|
||||
curl -o server.properties https://raw.githubusercontent.com/parkervcp/eggs/master/minecraft/java/server.properties
|
||||
fi
|
||||
container: 'ghcr.io/parkervcp/installers:alpine'
|
||||
entrypoint: ash
|
||||
variables:
|
||||
-
|
||||
name: 'Build Number'
|
||||
description: "The build number for the paper release.\r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest."
|
||||
env_variable: BUILD_NUMBER
|
||||
default_value: latest
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- 'max:20'
|
||||
sort: 4
|
||||
-
|
||||
name: 'Download Path'
|
||||
description: |-
|
||||
A URL to use to download a server.jar rather than the ones in the install script. This is not user
|
||||
viewable.
|
||||
env_variable: DL_PATH
|
||||
default_value: ''
|
||||
user_viewable: false
|
||||
user_editable: false
|
||||
rules:
|
||||
- nullable
|
||||
- string
|
||||
sort: 3
|
||||
-
|
||||
name: 'Minecraft Version'
|
||||
description: "The version of minecraft to download. \r\n\r\nLeave at latest to always get the latest version. Invalid versions will default to latest."
|
||||
env_variable: MINECRAFT_VERSION
|
||||
default_value: latest
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- nullable
|
||||
- string
|
||||
- 'max:20'
|
||||
sort: 1
|
||||
-
|
||||
name: 'Server Jar File'
|
||||
description: 'The name of the server jarfile to run the server with.'
|
||||
env_variable: SERVER_JARFILE
|
||||
default_value: server.jar
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'regex:/^([\w\d._-]+)(\.jar)$/'
|
||||
sort: 2
|
@ -1,96 +0,0 @@
|
||||
{
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PLCN_v1",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json"
|
||||
},
|
||||
"exported_at": "2025-04-25T06:05:10+00:00",
|
||||
"name": "Sponge",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
|
||||
"description": "A community-driven open source Minecraft: Java Edition modding platform.",
|
||||
"tags": [
|
||||
"minecraft"
|
||||
],
|
||||
"features": [
|
||||
"eula",
|
||||
"java_version",
|
||||
"pid_limit"
|
||||
],
|
||||
"docker_images": {
|
||||
"Java 21": "ghcr.io\/parkervcp\/yolks:java_21",
|
||||
"Java 17": "ghcr.io\/parkervcp\/yolks:java_17",
|
||||
"Java 16": "ghcr.io\/parkervcp\/yolks:java_16",
|
||||
"Java 11": "ghcr.io\/parkervcp\/yolks:java_11",
|
||||
"Java 8": "ghcr.io\/parkervcp\/yolks:java_8"
|
||||
},
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/ash\r\n# Sponge Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\ncd \/mnt\/server\r\n\r\nif [ $MINECRAFT_VERSION = 'latest' ] || [ -z $MINECRAFT_VERSION ]; then\r\n TARGET_VERSION_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/latest?recommended=true)\r\n if [ -z \"${TARGET_VERSION_JSON}\" ]; then\r\n echo -e \"Failed to find latest recommended version!\"\r\n exit 1\r\n fi\r\n echo -e \"Found latest version for ${SPONGE_TYPE}\"\r\nelse\r\n if [ $SPONGE_TYPE = 'spongevanilla' ]; then \r\n VERSIONS_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions?tags=,minecraft:${MINECRAFT_VERSION}&offset=0&limit=1)\r\n else\r\n FORGETAG='forge'\r\n if [ $SPONGE_TYPE = 'spongeneo' ]; then\r\n FORGETAG='neoforge'\r\n fi\r\n VERSIONS_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions?tags=,minecraft:${MINECRAFT_VERSION},${FORGETAG}:${FORGE_VERSION}&offset=0&limit=1)\r\n fi\r\n \r\n if [ -z \"${VERSIONS_JSON}\" ]; then\r\n echo -e \"Failed to find recommended ${MINECRAFT_VERSION} version for ${SPONGE_TYPE} ${FORGE_VERSION}!\"\r\n exit 1\r\n fi\r\n \r\n VERSION_KEY=$(echo $VERSIONS_JSON | jq -r '.artifacts | to_entries[0].key')\r\n TARGET_VERSION_JSON=$(curl -sSL https:\/\/dl-api.spongepowered.org\/v2\/groups\/org.spongepowered\/artifacts\/${SPONGE_TYPE}\/versions\/${VERSION_KEY})\r\n \r\n if [ -z \"${TARGET_VERSION_JSON}\" ]; then\r\n echo -e \"Failed to find ${VERSION_KEY} for ${SPONGE_TYPE} ${FORGE_VERSION}!\"\r\n exit 1\r\n fi\r\n\r\n echo -e \"Found ${MINECRAFT_VERSION} for ${SPONGE_TYPE}\"\r\nfi\r\n\r\nTARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == \"universal\")'`\r\nif [ -z \"${TARGET_VERSION}\" ]; then\r\n TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == \"\" and .extension == \"jar\")'`\r\nfi\r\n\r\nif [ -z \"${TARGET_VERSION}\" ]; then\r\n echo -e \"Failed to get download url data from the selected version\"\r\n exit 1\r\nfi\r\n\r\nSPONGE_URL=$(echo $TARGET_VERSION | jq -r '.downloadUrl')\r\nCHECKSUM=$(echo $TARGET_VERSION | jq -r '.md5')\r\necho -e \"Found file at ${SPONGE_URL} with checksum ${CHECKSUM}\"\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} ${SPONGE_URL}\"\r\ncurl -o ${SERVER_JARFILE} ${SPONGE_URL}\r\n\r\nif [ $(basename $(md5sum ${SERVER_JARFILE})) = ${CHECKSUM} ] ; then\r\n echo \"Checksum passed\"\r\nelse\r\n echo \"Checksum failed\"\r\nfi\r\n\r\necho -e \"Install Complete\"",
|
||||
"container": "ghcr.io\/parkervcp\/installers:alpine",
|
||||
"entrypoint": "ash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"sort": 3,
|
||||
"name": "Forge\/Neoforge Version",
|
||||
"description": "The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using `spongevanilla`",
|
||||
"env_variable": "FORGE_VERSION",
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sort": 1,
|
||||
"name": "Minecraft Version",
|
||||
"description": "The version of Minecraft to target. Use \"latest\" to install the latest version. Go to Settings > Reinstall Server to apply.",
|
||||
"env_variable": "MINECRAFT_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"between:3,15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sort": 4,
|
||||
"name": "Server Jar File",
|
||||
"description": "The name of the Jarfile to use when running Sponge.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"sort": 2,
|
||||
"name": "Sponge Type",
|
||||
"description": "SpongeVanilla if you are only using Sponge plugins.\nSpongeForge when using Forge mods and Sponge plugins.\nSpongeNeo when using NeoForge mods and Sponge plugins.",
|
||||
"env_variable": "SPONGE_TYPE",
|
||||
"default_value": "spongevanilla",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"in:spongevanilla,spongeforge,spongeneo"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
157
database/Seeders/eggs/minecraft/egg-sponge.yaml
Normal file
157
database/Seeders/eggs/minecraft/egg-sponge.yaml
Normal file
@ -0,0 +1,157 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v2
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.yaml'
|
||||
exported_at: '2025-08-05T21:00:17+00:00'
|
||||
name: Sponge
|
||||
author: panel@example.com
|
||||
uuid: f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d
|
||||
description: 'A community-driven open source Minecraft: Java Edition modding platform.'
|
||||
tags:
|
||||
- minecraft
|
||||
features:
|
||||
- eula
|
||||
- java_version
|
||||
- pid_limit
|
||||
docker_images:
|
||||
'Java 21': 'ghcr.io/parkervcp/yolks:java_21'
|
||||
'Java 17': 'ghcr.io/parkervcp/yolks:java_17'
|
||||
'Java 16': 'ghcr.io/parkervcp/yolks:java_16'
|
||||
'Java 11': 'ghcr.io/parkervcp/yolks:java_11'
|
||||
'Java 8': 'ghcr.io/parkervcp/yolks:java_8'
|
||||
file_denylist: { }
|
||||
startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
|
||||
config:
|
||||
files:
|
||||
server.properties:
|
||||
parser: properties
|
||||
find:
|
||||
server-ip: ''
|
||||
server-port: '{{server.allocations.default.port}}'
|
||||
query.port: '{{server.allocations.default.port}}'
|
||||
startup:
|
||||
done: ')! For help, type '
|
||||
logs: { }
|
||||
stop: stop
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/ash
|
||||
# Sponge Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
|
||||
cd /mnt/server
|
||||
|
||||
if [ $MINECRAFT_VERSION = 'latest' ] || [ -z $MINECRAFT_VERSION ]; then
|
||||
TARGET_VERSION_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/latest?recommended=true)
|
||||
if [ -z "${TARGET_VERSION_JSON}" ]; then
|
||||
echo -e "Failed to find latest recommended version!"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "Found latest version for ${SPONGE_TYPE}"
|
||||
else
|
||||
if [ $SPONGE_TYPE = 'spongevanilla' ]; then
|
||||
VERSIONS_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions?tags=,minecraft:${MINECRAFT_VERSION}&offset=0&limit=1)
|
||||
else
|
||||
FORGETAG='forge'
|
||||
if [ $SPONGE_TYPE = 'spongeneo' ]; then
|
||||
FORGETAG='neoforge'
|
||||
fi
|
||||
VERSIONS_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions?tags=,minecraft:${MINECRAFT_VERSION},${FORGETAG}:${FORGE_VERSION}&offset=0&limit=1)
|
||||
fi
|
||||
|
||||
if [ -z "${VERSIONS_JSON}" ]; then
|
||||
echo -e "Failed to find recommended ${MINECRAFT_VERSION} version for ${SPONGE_TYPE} ${FORGE_VERSION}!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_KEY=$(echo $VERSIONS_JSON | jq -r '.artifacts | to_entries[0].key')
|
||||
TARGET_VERSION_JSON=$(curl -sSL https://dl-api.spongepowered.org/v2/groups/org.spongepowered/artifacts/${SPONGE_TYPE}/versions/${VERSION_KEY})
|
||||
|
||||
if [ -z "${TARGET_VERSION_JSON}" ]; then
|
||||
echo -e "Failed to find ${VERSION_KEY} for ${SPONGE_TYPE} ${FORGE_VERSION}!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "Found ${MINECRAFT_VERSION} for ${SPONGE_TYPE}"
|
||||
fi
|
||||
|
||||
TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == "universal")'`
|
||||
if [ -z "${TARGET_VERSION}" ]; then
|
||||
TARGET_VERSION=`echo $TARGET_VERSION_JSON | jq '.assets[] | select(.classifier == "" and .extension == "jar")'`
|
||||
fi
|
||||
|
||||
if [ -z "${TARGET_VERSION}" ]; then
|
||||
echo -e "Failed to get download url data from the selected version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SPONGE_URL=$(echo $TARGET_VERSION | jq -r '.downloadUrl')
|
||||
CHECKSUM=$(echo $TARGET_VERSION | jq -r '.md5')
|
||||
echo -e "Found file at ${SPONGE_URL} with checksum ${CHECKSUM}"
|
||||
|
||||
echo -e "running: curl -o ${SERVER_JARFILE} ${SPONGE_URL}"
|
||||
curl -o ${SERVER_JARFILE} ${SPONGE_URL}
|
||||
|
||||
if [ $(basename $(md5sum ${SERVER_JARFILE})) = ${CHECKSUM} ] ; then
|
||||
echo "Checksum passed"
|
||||
else
|
||||
echo "Checksum failed"
|
||||
fi
|
||||
|
||||
echo -e "Install Complete"
|
||||
container: 'ghcr.io/parkervcp/installers:alpine'
|
||||
entrypoint: ash
|
||||
variables:
|
||||
-
|
||||
name: 'Forge/Neoforge Version'
|
||||
description: |-
|
||||
The modding api target version if set to `spongeforge` or `spongeneo`. Leave blank if using
|
||||
`spongevanilla`
|
||||
env_variable: FORGE_VERSION
|
||||
default_value: ''
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- string
|
||||
sort: 3
|
||||
-
|
||||
name: 'Minecraft Version'
|
||||
description: |-
|
||||
The version of Minecraft to target. Use "latest" to install the latest version. Go to Settings >
|
||||
Reinstall Server to apply.
|
||||
env_variable: MINECRAFT_VERSION
|
||||
default_value: latest
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- 'between:3,15'
|
||||
sort: 1
|
||||
-
|
||||
name: 'Server Jar File'
|
||||
description: 'The name of the Jarfile to use when running Sponge.'
|
||||
env_variable: SERVER_JARFILE
|
||||
default_value: server.jar
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'regex:/^([\w\d._-]+)(\.jar)$/'
|
||||
sort: 4
|
||||
-
|
||||
name: 'Sponge Type'
|
||||
description: |-
|
||||
SpongeVanilla if you are only using Sponge plugins.
|
||||
SpongeForge when using Forge mods and Sponge plugins.
|
||||
SpongeNeo when using NeoForge mods and Sponge plugins.
|
||||
env_variable: SPONGE_TYPE
|
||||
default_value: spongevanilla
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'in:spongevanilla,spongeforge,spongeneo'
|
||||
sort: 2
|
@ -1,71 +0,0 @@
|
||||
{
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL",
|
||||
"meta": {
|
||||
"version": "PLCN_v1",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-vanilla-minecraft.json"
|
||||
},
|
||||
"exported_at": "2025-03-18T12:35:55+00:00",
|
||||
"name": "Vanilla Minecraft",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b",
|
||||
"description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.",
|
||||
"tags": [
|
||||
"minecraft"
|
||||
],
|
||||
"features": [
|
||||
"eula",
|
||||
"java_version",
|
||||
"pid_limit"
|
||||
],
|
||||
"docker_images": {
|
||||
"Java 21": "ghcr.io\/parkervcp\/yolks:java_21",
|
||||
"Java 17": "ghcr.io\/parkervcp\/yolks:java_17",
|
||||
"Java 16": "ghcr.io\/parkervcp\/yolks:java_16",
|
||||
"Java 11": "ghcr.io\/parkervcp\/yolks:java_11",
|
||||
"Java 8": "ghcr.io\/parkervcp\/yolks:java_8"
|
||||
},
|
||||
"file_denylist": [],
|
||||
"startup": "java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}",
|
||||
"config": {
|
||||
"files": "{\r\n \"server.properties\": {\r\n \"parser\": \"properties\",\r\n \"find\": {\r\n \"server-ip\": \"0.0.0.0\",\r\n \"server-port\": \"{{server.allocations.default.port}}\",\r\n \"query.port\": \"{{server.allocations.default.port}}\"\r\n }\r\n }\r\n}",
|
||||
"startup": "{\r\n \"done\": \")! For help, type \"\r\n}",
|
||||
"logs": "{}",
|
||||
"stop": "stop"
|
||||
},
|
||||
"scripts": {
|
||||
"installation": {
|
||||
"script": "#!\/bin\/ash\r\n# Vanilla MC Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nLATEST_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.release'`\r\nLATEST_SNAPSHOT_VERSION=`curl https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq -r '.latest.snapshot'`\r\n\r\necho -e \"latest version is $LATEST_VERSION\"\r\necho -e \"latest snapshot is $LATEST_SNAPSHOT_VERSION\"\r\n\r\nif [ -z \"$VANILLA_VERSION\" ] || [ \"$VANILLA_VERSION\" == \"latest\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelif [ \"$VANILLA_VERSION\" == \"snapshot\" ]; then\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $LATEST_SNAPSHOT_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nelse\r\n MANIFEST_URL=$(curl -sSL https:\/\/launchermeta.mojang.com\/mc\/game\/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')\r\nfi\r\n\r\nDOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url')\r\n\r\necho -e \"running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL\"\r\ncurl -o ${SERVER_JARFILE} $DOWNLOAD_URL\r\n\r\necho -e \"Install Complete\"",
|
||||
"container": "ghcr.io\/parkervcp\/installers:alpine",
|
||||
"entrypoint": "ash"
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"name": "Server Jar File",
|
||||
"description": "The name of the server jarfile to run the server with.",
|
||||
"env_variable": "SERVER_JARFILE",
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
],
|
||||
"sort": 1
|
||||
},
|
||||
{
|
||||
"name": "Server Version",
|
||||
"description": "The version of Minecraft Vanilla to install. Use \"latest\" to install the latest version, or use \"snapshot\" to install the latest snapshot. Go to Settings > Reinstall Server to apply.",
|
||||
"env_variable": "VANILLA_VERSION",
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"between:3,15"
|
||||
],
|
||||
"sort": 2
|
||||
}
|
||||
]
|
||||
}
|
97
database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml
Normal file
97
database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml
Normal file
@ -0,0 +1,97 @@
|
||||
_comment: 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL'
|
||||
meta:
|
||||
version: PLCN_v2
|
||||
update_url: 'https://github.com/pelican-dev/panel/raw/main/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.yaml'
|
||||
exported_at: '2025-08-05T20:59:51+00:00'
|
||||
name: 'Vanilla Minecraft'
|
||||
author: panel@example.com
|
||||
uuid: 9ac39f3d-0c34-4d93-8174-c52ab9e6c57b
|
||||
description: |-
|
||||
Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds
|
||||
and build amazing things from the simplest of homes to the grandest of castles. Play in Creative
|
||||
Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off
|
||||
dangerous mobs. Do all this alone or with friends.
|
||||
tags:
|
||||
- minecraft
|
||||
features:
|
||||
- eula
|
||||
- java_version
|
||||
- pid_limit
|
||||
docker_images:
|
||||
'Java 21': 'ghcr.io/parkervcp/yolks:java_21'
|
||||
'Java 17': 'ghcr.io/parkervcp/yolks:java_17'
|
||||
'Java 16': 'ghcr.io/parkervcp/yolks:java_16'
|
||||
'Java 11': 'ghcr.io/parkervcp/yolks:java_11'
|
||||
'Java 8': 'ghcr.io/parkervcp/yolks:java_8'
|
||||
file_denylist: { }
|
||||
startup: 'java -Xms128M -XX:MaxRAMPercentage=95.0 -jar {{SERVER_JARFILE}}'
|
||||
config:
|
||||
files:
|
||||
server.properties:
|
||||
parser: properties
|
||||
find:
|
||||
server-ip: ''
|
||||
server-port: '{{server.allocations.default.port}}'
|
||||
query.port: '{{server.allocations.default.port}}'
|
||||
startup:
|
||||
done: ')! For help, type '
|
||||
logs: { }
|
||||
stop: stop
|
||||
scripts:
|
||||
installation:
|
||||
script: |-
|
||||
#!/bin/ash
|
||||
# Vanilla MC Installation Script
|
||||
#
|
||||
# Server Files: /mnt/server
|
||||
mkdir -p /mnt/server
|
||||
cd /mnt/server
|
||||
|
||||
LATEST_VERSION=`curl https://launchermeta.mojang.com/mc/game/version_manifest.json | jq -r '.latest.release'`
|
||||
LATEST_SNAPSHOT_VERSION=`curl https://launchermeta.mojang.com/mc/game/version_manifest.json | jq -r '.latest.snapshot'`
|
||||
|
||||
echo -e "latest version is $LATEST_VERSION"
|
||||
echo -e "latest snapshot is $LATEST_SNAPSHOT_VERSION"
|
||||
|
||||
if [ -z "$VANILLA_VERSION" ] || [ "$VANILLA_VERSION" == "latest" ]; then
|
||||
MANIFEST_URL=$(curl -sSL https://launchermeta.mojang.com/mc/game/version_manifest.json | jq --arg VERSION $LATEST_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')
|
||||
elif [ "$VANILLA_VERSION" == "snapshot" ]; then
|
||||
MANIFEST_URL=$(curl -sSL https://launchermeta.mojang.com/mc/game/version_manifest.json | jq --arg VERSION $LATEST_SNAPSHOT_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')
|
||||
else
|
||||
MANIFEST_URL=$(curl -sSL https://launchermeta.mojang.com/mc/game/version_manifest.json | jq --arg VERSION $VANILLA_VERSION -r '.versions | .[] | select(.id== $VERSION )|.url')
|
||||
fi
|
||||
|
||||
DOWNLOAD_URL=$(curl ${MANIFEST_URL} | jq .downloads.server | jq -r '. | .url')
|
||||
|
||||
echo -e "running: curl -o ${SERVER_JARFILE} $DOWNLOAD_URL"
|
||||
curl -o ${SERVER_JARFILE} $DOWNLOAD_URL
|
||||
|
||||
echo -e "Install Complete"
|
||||
container: 'ghcr.io/parkervcp/installers:alpine'
|
||||
entrypoint: ash
|
||||
variables:
|
||||
-
|
||||
name: 'Server Jar File'
|
||||
description: 'The name of the server jarfile to run the server with.'
|
||||
env_variable: SERVER_JARFILE
|
||||
default_value: server.jar
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- 'regex:/^([\w\d._-]+)(\.jar)$/'
|
||||
sort: 1
|
||||
-
|
||||
name: 'Server Version'
|
||||
description: |-
|
||||
The version of Minecraft Vanilla to install. Use "latest" to install the latest version, or use
|
||||
"snapshot" to install the latest snapshot. Go to Settings > Reinstall Server to apply.
|
||||
env_variable: VANILLA_VERSION
|
||||
default_value: latest
|
||||
user_viewable: true
|
||||
user_editable: true
|
||||
rules:
|
||||
- required
|
||||
- string
|
||||
- 'between:3,15'
|
||||
sort: 2
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user