mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
50 Commits
v1.0.0-bet
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
925ab26fb4 | ||
|
|
2952e22619 | ||
|
|
079eaed010 | ||
|
|
6671d45651 | ||
|
|
3543b4773a | ||
|
|
02f788a659 | ||
|
|
7ace3978d8 | ||
|
|
8f277aaca0 | ||
|
|
76451fa0ad | ||
|
|
0104a08ba4 | ||
|
|
5eff006843 | ||
|
|
a8241bf9f3 | ||
|
|
4aae2562ea | ||
|
|
42db5b328a | ||
|
|
bc4dfb3e92 | ||
|
|
3b9c81534f | ||
|
|
f31aa78f6f | ||
|
|
b5ebd544f4 | ||
|
|
c77a37ec89 | ||
|
|
4d78e5dcd1 | ||
|
|
15075b6ab8 | ||
|
|
a8f233e204 | ||
|
|
795cad43b9 | ||
|
|
46934d7a85 | ||
|
|
06067f375c | ||
|
|
d1df53c683 | ||
|
|
b03d2cf919 | ||
|
|
27a8423f55 | ||
|
|
ad70934430 | ||
|
|
900f8d0fe1 | ||
|
|
6a4ac515a7 | ||
|
|
7c315ac995 | ||
|
|
49e9440e0f | ||
|
|
02e3e43f1e | ||
|
|
8eddef6f04 | ||
|
|
d2f1936bbf | ||
|
|
36863f94c0 | ||
|
|
75863c50d1 | ||
|
|
ec0727b406 | ||
|
|
5b2e9d94ca | ||
|
|
8840d109ef | ||
|
|
71225bd2dc | ||
|
|
bab8ec6e18 | ||
|
|
d307a2095b | ||
|
|
a777f4e0ff | ||
|
|
86a71afc6c | ||
|
|
88943563c7 | ||
|
|
20071a64fa | ||
|
|
d0d3418e03 | ||
|
|
083e3dc62a |
7
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
7
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -64,10 +64,9 @@ body:
|
||||
label: Error Logs
|
||||
description: |
|
||||
Run the following command to collect logs on your system.
|
||||
|
||||
Wings: `sudo wings diagnostics`
|
||||
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
|
||||
placeholder: "https://pelipaste.com/a1h6z"
|
||||
Wings: `sudo wings diagnostics --hastebin-url=https://logs.pelican.dev`
|
||||
Panel: `tail -n 300 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl --data-binary @- https://logs.pelican.dev`
|
||||
placeholder: "https://logs.pelican.dev/c17f750e"
|
||||
render: bash
|
||||
validations:
|
||||
required: false
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class ProcessRunnableCommand extends Command
|
||||
} catch (Throwable $exception) {
|
||||
logger()->error($exception, ['schedule_id' => $schedule->id]);
|
||||
|
||||
$this->error(trans('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
|
||||
$this->error(trans('commands.schedule.process.error_message', ['schedules' => " #$schedule->id: " . $exception->getMessage()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class DisableTwoFactorCommand extends Command
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->input->isInteractive()) {
|
||||
$this->output->warning(trans('command/messages.user.2fa_help_text.0') . trans('command/messages.user.2fa_help_text.1'));
|
||||
$this->output->warning(trans('command/messages.user.2fa_help_text'));
|
||||
}
|
||||
|
||||
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));
|
||||
|
||||
@@ -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.' . $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.' . $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',
|
||||
|
||||
10
app/Enums/StartupVariableType.php
Normal file
10
app/Enums/StartupVariableType.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum StartupVariableType: string
|
||||
{
|
||||
case Text = 'text';
|
||||
case Select = 'select';
|
||||
case Toggle = 'toggle'; // TODO: add toggle to blade view
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class Health extends Page
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
return trans('admin/health.checks.failed') . implode(', ', $failedNames);
|
||||
return trans('admin/health.checks.failed', ['checks' => implode(', ', $failedNames)]);
|
||||
}
|
||||
|
||||
public static function getNavigationIcon(): string
|
||||
|
||||
@@ -119,7 +119,7 @@ class Settings extends Page implements HasForms
|
||||
->label(trans('admin/setting.navigation.backup'))
|
||||
->icon('tabler-box')
|
||||
->schema($this->backupSettings()),
|
||||
Tab::make('OAuth')
|
||||
Tab::make('oauth')
|
||||
->label(trans('admin/setting.navigation.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->schema($this->oauthSettings()),
|
||||
@@ -169,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)
|
||||
@@ -563,7 +553,7 @@ class Settings extends Page implements HasForms
|
||||
->label(trans('admin/setting.oauth.enable'))
|
||||
->color('success')
|
||||
->steps($schema->getSetupSteps())
|
||||
->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $schema->getName())
|
||||
->modalHeading(trans('admin/setting.oauth.enable_schema', ['schema' => $schema->getName()]))
|
||||
->modalSubmitActionLabel(trans('admin/setting.oauth.enable'))
|
||||
->modalCancelAction(false)
|
||||
->action(function ($data, Set $set) use ($key) {
|
||||
|
||||
@@ -96,7 +96,7 @@ class ApiKeyResource extends Resource
|
||||
])
|
||||
->emptyStateIcon('tabler-key')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading(trans('admin/apikey.empty_table'))
|
||||
->emptyStateHeading(trans('admin/apikey.empty'))
|
||||
->emptyStateActions([
|
||||
CreateAction::make(),
|
||||
]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -57,7 +57,8 @@ class CreateEgg extends CreateRecord
|
||||
return $form
|
||||
->schema([
|
||||
Tabs::make()->tabs([
|
||||
Tab::make(trans('admin/egg.tabs.configuration'))
|
||||
Tab::make('configuration')
|
||||
->label(trans('admin/egg.tabs.configuration'))
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
@@ -123,7 +124,8 @@ class CreateEgg extends CreateRecord
|
||||
->helperText(trans('admin/egg.docker_help')),
|
||||
]),
|
||||
|
||||
Tab::make(trans('admin/egg.tabs.process_management'))
|
||||
Tab::make('process_management')
|
||||
->label(trans('admin/egg.tabs.process_management'))
|
||||
->columns()
|
||||
->schema([
|
||||
CopyFrom::make('copy_process_from')
|
||||
@@ -146,7 +148,8 @@ class CreateEgg extends CreateRecord
|
||||
->default('{}')
|
||||
->helperText(trans('admin/egg.log_config_help')),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.tabs.egg_variables'))
|
||||
Tab::make('egg_variables')
|
||||
->label(trans('admin/egg.tabs.egg_variables'))
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
@@ -207,7 +210,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')),
|
||||
@@ -239,7 +242,8 @@ class CreateEgg extends CreateRecord
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.tabs.install_script'))
|
||||
Tab::make('install_script')
|
||||
->label(trans('admin/egg.tabs.install_script'))
|
||||
->columns(3)
|
||||
->schema([
|
||||
CopyFrom::make('copy_script_from')
|
||||
|
||||
@@ -44,7 +44,8 @@ class EditEgg extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Tabs::make()->tabs([
|
||||
Tab::make(trans('admin/egg.tabs.configuration'))
|
||||
Tab::make('configuration')
|
||||
->label(trans('admin/egg.tabs.configuration'))
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->icon('tabler-egg')
|
||||
->schema([
|
||||
@@ -115,7 +116,8 @@ class EditEgg extends EditRecord
|
||||
->valueLabel(trans('admin/egg.docker_uri'))
|
||||
->helperText(trans('admin/egg.docker_help')),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.tabs.process_management'))
|
||||
Tab::make('process_management')
|
||||
->label(trans('admin/egg.tabs.process_management'))
|
||||
->columns()
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
@@ -135,7 +137,8 @@ class EditEgg extends EditRecord
|
||||
->label(trans('admin/egg.log_config'))
|
||||
->helperText(trans('admin/egg.log_config_help')),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.tabs.egg_variables'))
|
||||
Tab::make('egg_variables')
|
||||
->label(trans('admin/egg.tabs.egg_variables'))
|
||||
->columnSpanFull()
|
||||
->icon('tabler-variable')
|
||||
->schema([
|
||||
@@ -196,7 +199,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')),
|
||||
@@ -228,7 +231,8 @@ class EditEgg extends EditRecord
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.tabs.install_script'))
|
||||
Tab::make('install_script')
|
||||
->label(trans('admin/egg.tabs.install_script'))
|
||||
->columns(3)
|
||||
->icon('tabler-file-download')
|
||||
->schema([
|
||||
|
||||
@@ -22,7 +22,7 @@ class ServersRelationManager extends RelationManager
|
||||
->heading(trans('admin/egg.servers'))
|
||||
->columns([
|
||||
TextColumn::make('user.username')
|
||||
->label('Owner')
|
||||
->label(trans('admin/server.owner'))
|
||||
->icon('tabler-user')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -404,7 +404,7 @@ class CreateNode extends CreateRecord
|
||||
type="submit"
|
||||
size="sm"
|
||||
>
|
||||
Create Node
|
||||
{{ trans('admin/node.create') }}
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
]);
|
||||
|
||||
@@ -64,7 +64,7 @@ class EditNode extends EditRecord
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tab::make('')
|
||||
Tab::make('overview')
|
||||
->label(trans('admin/node.tabs.overview'))
|
||||
->icon('tabler-chart-area-line-filled')
|
||||
->columns([
|
||||
@@ -80,7 +80,7 @@ class EditNode extends EditRecord
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->label(trans('admin/node.wings_version'))
|
||||
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' (' . trans('admin/node.latest') . ': ' . $versionService->latestWingsVersion() . ')'),
|
||||
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])),
|
||||
Placeholder::make('')
|
||||
->label(trans('admin/node.cpu_threads'))
|
||||
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
|
||||
@@ -108,7 +108,8 @@ class EditNode extends EditRecord
|
||||
View::make('filament.components.node-storage-chart')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Tab::make(trans('admin/node.tabs.basic_settings'))
|
||||
Tab::make('basic_settings')
|
||||
->label(trans('admin/node.tabs.basic_settings'))
|
||||
->icon('tabler-server')
|
||||
->schema([
|
||||
TextInput::make('fqdn')
|
||||
@@ -257,7 +258,7 @@ class EditNode extends EditRecord
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
]),
|
||||
Tab::make('adv')
|
||||
Tab::make('advanced_settings')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
->columns([
|
||||
'default' => 1,
|
||||
@@ -525,7 +526,7 @@ class EditNode extends EditRecord
|
||||
->suffix('%'),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Config')
|
||||
Tab::make('config_file')
|
||||
->label(trans('admin/node.tabs.config_file'))
|
||||
->icon('tabler-code')
|
||||
->schema([
|
||||
@@ -553,7 +554,7 @@ class EditNode extends EditRecord
|
||||
->modalFooterActionsAlignment(Alignment::Center)
|
||||
->form([
|
||||
ToggleButtons::make('docker')
|
||||
->label('Type')
|
||||
->label(trans('admin/node.auto_label'))
|
||||
->live()
|
||||
->helperText(trans('admin/node.auto_question'))
|
||||
->inline()
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets;
|
||||
use App\Models\Node;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class NodeCpuChart extends ChartWidget
|
||||
{
|
||||
@@ -82,8 +81,8 @@ class NodeCpuChart extends ChartWidget
|
||||
{
|
||||
$data = array_slice(end($this->cpuHistory), -60);
|
||||
|
||||
$cpu = Number::format($data['cpu'], maxPrecision: 2, locale: auth()->user()->language);
|
||||
$max = Number::format($this->threads * 100, locale: auth()->user()->language);
|
||||
$cpu = format_number($data['cpu'], maxPrecision: 2);
|
||||
$max = format_number($this->threads * 100);
|
||||
|
||||
return trans('admin/node.cpu_chart', ['cpu' => $cpu, 'max' => $max]);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Filament\Admin\Resources\NodeResource\Widgets;
|
||||
use App\Models\Node;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class NodeMemoryChart extends ChartWidget
|
||||
{
|
||||
@@ -85,12 +84,12 @@ class NodeMemoryChart extends ChartWidget
|
||||
$latestMemoryUsed = array_slice(end($this->memoryHistory), -60);
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed['memory'], maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
? format_number($latestMemoryUsed['memory'], maxPrecision: 2) .' GiB'
|
||||
: format_number($latestMemoryUsed['memory'], maxPrecision: 2) . ' GB';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
? format_number($this->totalMemory / 1024 / 1024 / 1024, maxPrecision: 2) .' GiB'
|
||||
: format_number($this->totalMemory / 1000 / 1000 / 1000, maxPrecision: 2) . ' GB';
|
||||
|
||||
return trans('admin/node.memory_chart', ['used' => $used, 'total' => $total]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -129,7 +129,6 @@ class RoleResource extends Resource
|
||||
->required()
|
||||
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
TextInput::make('guard_name')
|
||||
->label('Guard Name')
|
||||
->default(Role::DEFAULT_GUARD_NAME)
|
||||
->nullable()
|
||||
->hidden(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Admin\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\ServerResource;
|
||||
use App\Filament\Components\Forms\Fields\StartupVariable;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
@@ -13,11 +14,9 @@ use App\Services\Servers\ServerCreationService;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
@@ -41,7 +40,6 @@ use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use LogicException;
|
||||
|
||||
@@ -266,7 +264,7 @@ class CreateServer extends CreateRecord
|
||||
->preload()
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->prefixIcon('tabler-network')
|
||||
->label('Additional Allocations')
|
||||
->label(trans('admin/server.additional_allocations'))
|
||||
->columnSpan(2)
|
||||
->disabled(fn (Get $get) => $get('../../allocation_id') === null || $get('../../node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
@@ -429,7 +427,7 @@ class CreateServer extends CreateRecord
|
||||
),
|
||||
|
||||
Repeater::make('server_variables')
|
||||
->label('')
|
||||
->hiddenLabel()
|
||||
->relationship('serverVariables', fn (Builder $query) => $query->orderByPowerJoins('variable.sort'))
|
||||
->saveRelationshipsBeforeChildrenUsing(null)
|
||||
->saveRelationshipsUsing(null)
|
||||
@@ -439,51 +437,15 @@ class CreateServer extends CreateRecord
|
||||
->deletable(false)
|
||||
->default([])
|
||||
->hidden(fn ($state) => empty($state))
|
||||
->schema(function () {
|
||||
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->required(fn (Get $get) => in_array('required', $get('rules')))
|
||||
->rules(
|
||||
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $get('rules'),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $get('name'))->toString();
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
$select = Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
||||
$components = [$text, $select];
|
||||
|
||||
foreach ($components as &$component) {
|
||||
$component = $component
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (Get $get) => $get('name'))
|
||||
->hintIconTooltip(fn (Get $get) => implode('|', $get('rules')))
|
||||
->prefix(fn (Get $get) => '{{' . $get('env_variable') . '}}')
|
||||
->helperText(fn (Get $get) => empty($get('description')) ? '—' : $get('description'))
|
||||
->afterStateUpdated(function (Set $set, Get $get, $state) {
|
||||
$environment = $get($envPath = '../../environment');
|
||||
$environment[$get('env_variable')] = $state;
|
||||
$set($envPath, $environment);
|
||||
});
|
||||
}
|
||||
|
||||
return $components;
|
||||
})
|
||||
->schema([
|
||||
StartupVariable::make('variable_value')
|
||||
->fromForm()
|
||||
->afterStateUpdated(function (Set $set, Get $get, $state) {
|
||||
$environment = $get($envPath = '../../environment');
|
||||
$environment[$get('env_variable')] = $state;
|
||||
$set($envPath, $environment);
|
||||
}),
|
||||
])
|
||||
->columnSpan(2),
|
||||
]),
|
||||
]),
|
||||
@@ -799,7 +761,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
KeyValue::make('docker_labels')
|
||||
->live()
|
||||
->label('Container Labels')
|
||||
->label(trans('admin/server.container_labels'))
|
||||
->keyLabel(trans('admin/server.title'))
|
||||
->valueLabel(trans('admin/server.description'))
|
||||
->columnSpanFull(),
|
||||
@@ -815,7 +777,7 @@ class CreateServer extends CreateRecord
|
||||
type="submit"
|
||||
size="sm"
|
||||
>
|
||||
Create Server
|
||||
{{ trans('admin/server.create') }}
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
]);
|
||||
@@ -851,40 +813,6 @@ class CreateServer extends CreateRecord
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldHideComponent(Get $get, Component $component): bool
|
||||
{
|
||||
$containsRuleIn = collect($get('rules'))->reduce(
|
||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||
);
|
||||
|
||||
if ($component instanceof Select) {
|
||||
return $containsRuleIn;
|
||||
}
|
||||
|
||||
if ($component instanceof TextInput) {
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
|
||||
throw new Exception('Component type not supported: ' . $component::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, string>
|
||||
*/
|
||||
private function getSelectOptionsFromRules(Get $get): array
|
||||
{
|
||||
$inRule = collect($get('rules'))->reduce(
|
||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||
);
|
||||
|
||||
return str($inRule)
|
||||
->after('in:')
|
||||
->explode(',')
|
||||
->each(fn ($value) => str($value)->trim())
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $portEntries
|
||||
* @return array<int>
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Enums\SuspendAction;
|
||||
use App\Filament\Admin\Resources\ServerResource;
|
||||
use App\Filament\Components\Forms\Actions\PreviewStartupAction;
|
||||
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction;
|
||||
use App\Filament\Components\Forms\Fields\StartupVariable;
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Database;
|
||||
@@ -27,10 +28,8 @@ use App\Services\Servers\ToggleInstallService;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions as FormActions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Component;
|
||||
@@ -56,7 +55,6 @@ use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use LogicException;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
@@ -89,7 +87,8 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tab::make(trans('admin/server.tabs.information'))
|
||||
Tab::make('information')
|
||||
->label(trans('admin/server.tabs.information'))
|
||||
->icon('tabler-info-circle')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
@@ -221,7 +220,8 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->disabled(),
|
||||
]),
|
||||
Tab::make(trans('admin/server.tabs.environment_configuration'))
|
||||
Tab::make('environment_configuration')
|
||||
->label(trans('admin/server.tabs.environment_configuration'))
|
||||
->icon('tabler-brand-docker')
|
||||
->schema([
|
||||
Fieldset::make(trans('admin/server.resource_limits'))
|
||||
@@ -528,7 +528,8 @@ class EditServer extends EditRecord
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
Tab::make(trans('admin/server.egg'))
|
||||
Tab::make('egg')
|
||||
->label(trans('admin/server.egg'))
|
||||
->icon('tabler-egg')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
@@ -617,7 +618,7 @@ class EditServer extends EditRecord
|
||||
}),
|
||||
|
||||
Repeater::make('server_variables')
|
||||
->label('')
|
||||
->hiddenLabel()
|
||||
->relationship('serverVariables', function (Builder $query) {
|
||||
/** @var Server $server */
|
||||
$server = $this->getRecord();
|
||||
@@ -634,64 +635,26 @@ class EditServer extends EditRecord
|
||||
return $query->orderByPowerJoins('variable.sort');
|
||||
})
|
||||
->grid()
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||
foreach ($data as $key => $value) {
|
||||
if (!isset($data['variable_value'])) {
|
||||
$data['variable_value'] = '';
|
||||
}
|
||||
}
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['variable_value'] ??= '';
|
||||
|
||||
return $data;
|
||||
})
|
||||
->reorderable(false)->addable(false)->deletable(false)
|
||||
->schema(function () {
|
||||
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
|
||||
->rules([
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
'validatorkey' => $serverVariable->variable->rules,
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
|
||||
|
||||
$fail($message);
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
$select = Select::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->dehydratedWhenHidden()
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
||||
$components = [$text, $select];
|
||||
|
||||
foreach ($components as &$component) {
|
||||
$component = $component
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
|
||||
return $components;
|
||||
})
|
||||
->schema([
|
||||
StartupVariable::make('variable_value')
|
||||
->fromRecord(),
|
||||
])
|
||||
->columnSpan(6),
|
||||
]),
|
||||
Tab::make(trans('admin/server.mounts'))
|
||||
Tab::make('mounts')
|
||||
->label(trans('admin/server.mounts'))
|
||||
->icon('tabler-layers-linked')
|
||||
->schema(fn (Get $get) => [
|
||||
ServerResource::getMountCheckboxList($get),
|
||||
]),
|
||||
Tab::make(trans('admin/server.databases'))
|
||||
Tab::make('databases')
|
||||
->label(trans('admin/server.databases'))
|
||||
->hidden(fn () => !auth()->user()->can('viewAny', Database::class))
|
||||
->icon('tabler-database')
|
||||
->columns(4)
|
||||
@@ -821,7 +784,8 @@ class EditServer extends EditRecord
|
||||
]),
|
||||
])->alignCenter()->columnSpanFull(),
|
||||
]),
|
||||
Tab::make(trans('admin/server.actions'))
|
||||
Tab::make('actions')
|
||||
->label(trans('admin/server.actions'))
|
||||
->icon('tabler-settings')
|
||||
->schema([
|
||||
Fieldset::make(trans('admin/server.actions'))
|
||||
@@ -954,12 +918,12 @@ class EditServer extends EditRecord
|
||||
$transfer->handle($server, Arr::get($data, 'node_id'), Arr::get($data, 'allocation_id'), Arr::get($data, 'allocation_additional', []));
|
||||
|
||||
Notification::make()
|
||||
->title('Transfer started')
|
||||
->title(trans('admin/server.notifications.transfer_started'))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Transfer failed')
|
||||
->title(trans('admin/server.notifications.transfer_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
@@ -1145,34 +1109,4 @@ class EditServer extends EditRecord
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
|
||||
|
||||
if ($component instanceof Select) {
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
|
||||
if ($component instanceof TextInput) {
|
||||
return $containsRuleIn;
|
||||
}
|
||||
|
||||
throw new Exception('Component type not supported: ' . $component::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
|
||||
{
|
||||
$inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'));
|
||||
|
||||
return str($inRule)
|
||||
->after('in:')
|
||||
->explode(',')
|
||||
->each(fn ($value) => str($value)->trim())
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -80,7 +80,7 @@ class UserResource extends Resource
|
||||
->label(trans('admin/user.email'))
|
||||
->icon('tabler-mail'),
|
||||
IconColumn::make('use_totp')
|
||||
->label('2FA')
|
||||
->label(trans('profile.tabs.2fa'))
|
||||
->visibleFrom('lg')
|
||||
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
||||
->boolean(),
|
||||
|
||||
@@ -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}}',
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ class WebhookResource extends Resource
|
||||
TextInput::make('thread_name')
|
||||
->label(trans('admin/webhook.discord_message.forum_thread')),
|
||||
CheckboxList::make('flags')
|
||||
->label('Flags')
|
||||
->label(trans('admin/webhook.discord_embed.flags'))
|
||||
->options([
|
||||
(1 << 2) => trans('admin/webhook.discord_message.supress_embeds'),
|
||||
(1 << 12) => trans('admin/webhook.discord_message.supress_notifications'),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +61,13 @@ class ListServers extends ListRecords
|
||||
{
|
||||
return [
|
||||
TextColumn::make('condition')
|
||||
->label('Status')
|
||||
->label(trans('server/dashboard.status'))
|
||||
->badge()
|
||||
->tooltip(fn (Server $server) => $server->formatResource('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')
|
||||
->label('Server')
|
||||
->label(trans('server/dashboard.title'))
|
||||
->description(fn (Server $server) => $server->description)
|
||||
->grow()
|
||||
->searchable(),
|
||||
@@ -78,22 +78,22 @@ class ListServers extends ListRecords
|
||||
->copyable(request()->isSecure())
|
||||
->state(fn (Server $server) => $server->allocation->address ?? 'None'),
|
||||
TextColumn::make('cpuUsage')
|
||||
->label('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')),
|
||||
];
|
||||
}
|
||||
@@ -142,15 +142,18 @@ class ListServers extends ListRecords
|
||||
$other = (clone $all)->whereNot('owner_id', auth()->user()->id);
|
||||
|
||||
return [
|
||||
'my' => Tab::make('My Servers')
|
||||
'my' => Tab::make('my')
|
||||
->label(trans('server/dashboard.tabs.my'))
|
||||
->badge(fn () => $my->count())
|
||||
->modifyQueryUsing(fn () => $my),
|
||||
|
||||
'other' => Tab::make('Others\' Servers')
|
||||
'other' => Tab::make('other')
|
||||
->label(trans('server/dashboard.tabs.other'))
|
||||
->badge(fn () => $other->count())
|
||||
->modifyQueryUsing(fn () => $other),
|
||||
|
||||
'all' => Tab::make('All Servers')
|
||||
'all' => Tab::make('all')
|
||||
->label(trans('server/dashboard.tabs.all'))
|
||||
->badge($all->count()),
|
||||
];
|
||||
}
|
||||
@@ -204,8 +207,8 @@ class ListServers extends ListRecords
|
||||
$this->daemonPowerRepository->setServer($server)->send($action);
|
||||
|
||||
Notification::make()
|
||||
->title('Power Action')
|
||||
->body($action . ' sent to ' . $server->name)
|
||||
->title(trans('server/dashboard.power_actions'))
|
||||
->body(trans('server/dashboard.power_action_sent', ['action' => $action, 'name' => $server->name]))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
|
||||
@@ -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', ['format' => 'json']))
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::JSON);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->close(),
|
||||
Action::make('yaml')
|
||||
->label(trans('admin/egg.export.as', ['format' => 'yaml']))
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::YAML);
|
||||
}, 'egg-' . $egg->getKebabName() . '.yaml'))
|
||||
->close(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -88,19 +106,21 @@ class ImportEggAction extends Action
|
||||
Tabs::make('Tabs')
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
Tab::make('file')
|
||||
->label(trans('admin/egg.import.file'))
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
->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)
|
||||
->multiple($isMultiple),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
Tab::make('url')
|
||||
->label(trans('admin/egg.import.url'))
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
Select::make('github')
|
||||
@@ -125,7 +145,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 +159,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,28 @@ class ImportScheduleAction extends Action
|
||||
Tabs::make('Tabs')
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/schedule.import.file'))
|
||||
Tab::make('file')
|
||||
->label(trans('server/schedule.import_action.file'))
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
->label(trans('admin/schedule.model_label'))
|
||||
->hint(trans('admin/schedule.import.schedule_help'))
|
||||
->hiddenLabel()
|
||||
->hint(trans('server/schedule.import_action.schedule_help'))
|
||||
->acceptedFileTypes(['application/json'])
|
||||
->preserveFilenames()
|
||||
->previewable(false)
|
||||
->storeFiles(false)
|
||||
->multiple(true),
|
||||
]),
|
||||
Tab::make(trans('admin/schedule.import.url'))
|
||||
Tab::make('url')
|
||||
->label(trans('server/schedule.import_action.url'))
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
Repeater::make('urls')
|
||||
->label('')
|
||||
->hiddenLabel()
|
||||
->itemLabel(fn (array $state) => str($state['url'])->afterLast('/schedule-')->before('.json')->headline())
|
||||
->hint(trans('admin/schedule.import.url_help'))
|
||||
->addActionLabel(trans('admin/schedule.import.add_url'))
|
||||
->hint(trans('server/schedule.import_action.url_help'))
|
||||
->addActionLabel(trans('server/schedule.import_action.add_url'))
|
||||
->grid(2)
|
||||
->reorderable(false)
|
||||
->addable(true)
|
||||
@@ -66,10 +68,10 @@ class ImportScheduleAction extends Action
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->live()
|
||||
->label(trans('admin/schedule.import.url'))
|
||||
->label(trans('server/schedule.import_action.url'))
|
||||
->url()
|
||||
->endsWith('.json')
|
||||
->validationAttribute(trans('admin/schedule.import.url')),
|
||||
->validationAttribute(trans('server/schedule.import_action.url')),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
@@ -104,14 +106,14 @@ class ImportScheduleAction extends Action
|
||||
|
||||
if ($failed->count() > 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/schedule.import.import_failed'))
|
||||
->title(trans('server/schedule.import_action.import_failed'))
|
||||
->body($failed->join(', '))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
if ($success->count() > 0) {
|
||||
Notification::make()
|
||||
->title(trans('admin/schedule.import.import_success'))
|
||||
->title(trans('server/schedule.import_action.import_success'))
|
||||
->body($success->join(', '))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@@ -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();
|
||||
|
||||
181
app/Filament/Components/Forms/Fields/StartupVariable.php
Normal file
181
app/Filament/Components/Forms/Fields/StartupVariable.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Forms\Fields;
|
||||
|
||||
use App\Enums\StartupVariableType;
|
||||
use App\Models\ServerVariable;
|
||||
use Closure;
|
||||
use Filament\Forms\Components\Concerns\HasAffixes;
|
||||
use Filament\Forms\Components\Concerns\HasExtraInputAttributes;
|
||||
use Filament\Forms\Components\Concerns\HasPlaceholder;
|
||||
use Filament\Forms\Components\Field;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Support\Concerns\HasExtraAlpineAttributes;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StartupVariable extends Field
|
||||
{
|
||||
use HasAffixes;
|
||||
use HasExtraAlpineAttributes;
|
||||
use HasExtraInputAttributes;
|
||||
use HasPlaceholder;
|
||||
|
||||
/** @var view-string */
|
||||
protected string $view = 'filament.components.startup-variable';
|
||||
|
||||
protected string|Closure|null $variableName = null;
|
||||
|
||||
protected string|Closure|null $variableDesc = null;
|
||||
|
||||
protected string|Closure|null $variableEnv = null;
|
||||
|
||||
protected string|Closure|null $variableDefault = null;
|
||||
|
||||
/** @var string[]|Closure|null */
|
||||
protected array|Closure|null $variableRules = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label(fn (StartupVariable $component) => $component->getVariableName());
|
||||
|
||||
$this->prefix(fn (StartupVariable $component) => '{{' . $component->getVariableEnv() . '}}');
|
||||
|
||||
$this->hintIcon('tabler-code');
|
||||
|
||||
$this->hintIconTooltip(fn (StartupVariable $component) => implode('|', $component->getVariableRules()));
|
||||
|
||||
$this->helperText(fn (StartupVariable $component) => !$component->getVariableDesc() ? '—' : $component->getVariableDesc());
|
||||
|
||||
$this->rules(fn (StartupVariable $component) => $component->getVariableRules());
|
||||
|
||||
$this->placeholder(fn (StartupVariable $component) => $component->getVariableDefault());
|
||||
|
||||
$this->live(onBlur: true);
|
||||
}
|
||||
|
||||
public function fromForm(): static
|
||||
{
|
||||
$this->variableName(fn (Get $get) => $get('name'));
|
||||
$this->variableDesc(fn (Get $get) => $get('description'));
|
||||
$this->variableEnv(fn (Get $get) => $get('env_variable'));
|
||||
$this->variableDefault(fn (Get $get) => $get('default_value'));
|
||||
$this->variableRules(fn (Get $get) => $get('rules'));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function fromRecord(): static
|
||||
{
|
||||
$this->variableName(fn (ServerVariable $record) => $record->variable->name);
|
||||
$this->variableDesc(fn (ServerVariable $record) => $record->variable->description);
|
||||
$this->variableEnv(fn (ServerVariable $record) => $record->variable->env_variable);
|
||||
$this->variableDefault(fn (ServerVariable $record) => $record->variable->default_value);
|
||||
$this->variableRules(fn (ServerVariable $record) => $record->variable->rules);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function variableName(string|Closure|null $name): static
|
||||
{
|
||||
$this->variableName = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function variableDesc(string|Closure|null $desc): static
|
||||
{
|
||||
$this->variableDesc = $desc;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function variableEnv(string|Closure|null $envVariable): static
|
||||
{
|
||||
$this->variableEnv = $envVariable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function variableDefault(string|Closure|null $default): static
|
||||
{
|
||||
$this->variableDefault = $default;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param string[]|Closure|null $rules */
|
||||
public function variableRules(array|Closure|null $rules): static
|
||||
{
|
||||
$this->variableRules = $rules;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVariableName(): ?string
|
||||
{
|
||||
return $this->evaluate($this->variableName);
|
||||
}
|
||||
|
||||
public function getVariableDesc(): ?string
|
||||
{
|
||||
return $this->evaluate($this->variableDesc);
|
||||
}
|
||||
|
||||
public function getVariableEnv(): ?string
|
||||
{
|
||||
return $this->evaluate($this->variableEnv);
|
||||
}
|
||||
|
||||
public function getVariableDefault(): ?string
|
||||
{
|
||||
return $this->evaluate($this->variableDefault);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getVariableRules(): array
|
||||
{
|
||||
return (array) ($this->evaluate($this->variableRules) ?? []);
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
$rules = $this->getVariableRules();
|
||||
|
||||
return in_array('required', $rules);
|
||||
}
|
||||
|
||||
public function getType(): StartupVariableType
|
||||
{
|
||||
$rules = $this->getVariableRules();
|
||||
|
||||
if (Arr::first($rules, fn ($value) => str($value)->startsWith('in:'), false)) {
|
||||
return StartupVariableType::Select;
|
||||
}
|
||||
|
||||
if (in_array('boolean', $rules)) {
|
||||
return StartupVariableType::Toggle;
|
||||
}
|
||||
|
||||
return StartupVariableType::Text;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getSelectOptions(): array
|
||||
{
|
||||
$rules = $this->getVariableRules();
|
||||
|
||||
$inRule = Arr::first($rules, fn ($value) => str($value)->startsWith('in:'));
|
||||
if ($inRule) {
|
||||
return str($inRule)
|
||||
->after('in:')
|
||||
->explode(',')
|
||||
->each(fn ($value) => Str::trim($value))
|
||||
->all();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -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', ['format' => 'json']))
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::JSON);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->close(),
|
||||
Action::make('yaml')
|
||||
->label(trans('admin/egg.export.as', ['format' => 'yaml']))
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id, EggFormat::YAML);
|
||||
}, 'egg-' . $egg->getKebabName() . '.yaml'))
|
||||
->close(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,8 @@ class ImportEggAction extends Action
|
||||
Tabs::make('Tabs')
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
Tab::make('file')
|
||||
->label(trans('admin/egg.import.file'))
|
||||
->icon('tabler-file-upload')
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
@@ -98,7 +99,8 @@ class ImportEggAction extends Action
|
||||
->storeFiles(false)
|
||||
->multiple($isMultiple),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
Tab::make('url')
|
||||
->label(trans('admin/egg.import.url'))
|
||||
->icon('tabler-world-upload')
|
||||
->schema([
|
||||
Select::make('github')
|
||||
@@ -117,7 +119,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 +133,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')),
|
||||
]),
|
||||
]),
|
||||
|
||||
@@ -86,7 +86,8 @@ class EditProfile extends BaseEditProfile
|
||||
->schema([
|
||||
Tabs::make()->persistTabInQueryString()
|
||||
->schema([
|
||||
Tab::make(trans('profile.tabs.account'))
|
||||
Tab::make('account')
|
||||
->label(trans('profile.tabs.account'))
|
||||
->icon('tabler-user')
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
@@ -160,7 +161,8 @@ class EditProfile extends BaseEditProfile
|
||||
}),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.oauth'))
|
||||
Tab::make('oauth')
|
||||
->label(trans('profile.tabs.oauth'))
|
||||
->icon('tabler-brand-oauth')
|
||||
->visible(count($oauthSchemas) > 0)
|
||||
->schema(function () use ($oauthSchemas) {
|
||||
@@ -174,7 +176,7 @@ class EditProfile extends BaseEditProfile
|
||||
$unlink = array_key_exists($id, $this->getUser()->oauth ?? []);
|
||||
|
||||
$actions[] = Action::make("oauth_$id")
|
||||
->label(($unlink ? trans('profile.unlink') : trans('profile.link')) . $name)
|
||||
->label(trans('profile.' . ($unlink ? 'unlink' : 'link'), ['name' => $name]))
|
||||
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
|
||||
->color(Color::hex($schema->getHexColor()))
|
||||
->action(function (UserUpdateService $updateService) use ($id, $name, $unlink) {
|
||||
@@ -199,7 +201,8 @@ class EditProfile extends BaseEditProfile
|
||||
return [Actions::make($actions)];
|
||||
}),
|
||||
|
||||
Tab::make(trans('profile.tabs.2fa'))
|
||||
Tab::make('2fa')
|
||||
->label(trans('profile.tabs.2fa'))
|
||||
->icon('tabler-shield-lock')
|
||||
->schema(function (TwoFactorSetupService $setupService) {
|
||||
if ($this->getUser()->use_totp) {
|
||||
@@ -263,7 +266,7 @@ class EditProfile extends BaseEditProfile
|
||||
->content(fn () => new HtmlString("
|
||||
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
|
||||
"))
|
||||
->helperText(trans('profile.setup_key') .': '. $secret),
|
||||
->helperText(trans('profile.setup_key', ['secret' => $secret])),
|
||||
TextInput::make('2facode')
|
||||
->label(trans('profile.code'))
|
||||
->requiredWith('2fapassword')
|
||||
@@ -276,7 +279,8 @@ class EditProfile extends BaseEditProfile
|
||||
];
|
||||
}),
|
||||
|
||||
Tab::make(trans('profile.tabs.api_keys'))
|
||||
Tab::make('api_keys')
|
||||
->label(trans('profile.tabs.api_keys'))
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Grid::make('name')->columns(5)->schema([
|
||||
@@ -322,6 +326,7 @@ class EditProfile extends BaseEditProfile
|
||||
Section::make(trans('profile.api_keys'))->columnSpan(2)->schema([
|
||||
Repeater::make('api_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('apiKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['identifier'])
|
||||
@@ -357,7 +362,8 @@ class EditProfile extends BaseEditProfile
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.ssh_keys'))
|
||||
Tab::make('ssh_keys')
|
||||
->label(trans('profile.tabs.ssh_keys'))
|
||||
->icon('tabler-lock-code')
|
||||
->schema([
|
||||
Grid::make('name')->columns(5)->schema([
|
||||
@@ -406,6 +412,7 @@ class EditProfile extends BaseEditProfile
|
||||
Section::make(trans('profile.ssh_keys'))->columnSpan(2)->schema([
|
||||
Repeater::make('ssh_keys')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->relationship('sshKeys')
|
||||
->addable(false)
|
||||
->itemLabel(fn ($state) => $state['name'])
|
||||
@@ -441,22 +448,27 @@ class EditProfile extends BaseEditProfile
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.activity'))
|
||||
Tab::make('activity')
|
||||
->label(trans('profile.tabs.activity'))
|
||||
->icon('tabler-history')
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->label('')
|
||||
->hiddenLabel()
|
||||
->inlineLabel(false)
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
})
|
||||
->schema([
|
||||
Placeholder::make('activity!')->label('')->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
Placeholder::make('log')
|
||||
->hiddenLabel()
|
||||
->content(fn (ActivityLog $log) => new HtmlString($log->htmlable())),
|
||||
]),
|
||||
]),
|
||||
|
||||
Tab::make(trans('profile.tabs.customization'))
|
||||
Tab::make('customization')
|
||||
->label(trans('profile.tabs.customization'))
|
||||
->icon('tabler-adjustments')
|
||||
->schema([
|
||||
Section::make(trans('profile.dashboard'))
|
||||
@@ -471,6 +483,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 +648,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 +666,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;
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ class Login extends BaseLogin
|
||||
protected function getLoginFormComponent(): Component
|
||||
{
|
||||
return TextInput::make('login')
|
||||
->label('Login')
|
||||
->label(trans('filament-panels::pages/auth/login.title'))
|
||||
->required()
|
||||
->autocomplete()
|
||||
->autofocus()
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Support\Number;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class Settings extends ServerFormPage
|
||||
@@ -39,7 +38,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 +46,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 +62,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 +75,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 +84,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 +99,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') : format_number($server->cpu) . '%'),
|
||||
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 ? trans('server/backup.empty') : $server->backups->count() . ' ' .trans('server/setting.server_info.limits.of', ['max' => $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 ? trans('server/database.empty') : $server->databases->count() . ' ' . trans('server/setting.server_info.limits.of', ['max' => $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', ['max' => $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 +157,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 +178,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 +210,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 +222,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 +232,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 +257,15 @@ class Settings extends ServerFormPage
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Updated Server Name')
|
||||
->title(trans('server/setting.server_info.notification_name'))
|
||||
->body(fn () => $original . ' -> ' . $name)
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed')
|
||||
->title(trans('server/setting.server_info.failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
@@ -289,16 +288,26 @@ class Settings extends ServerFormPage
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Updated Server Description')
|
||||
->title(trans('server/setting.server_info.notification_description'))
|
||||
->body(fn () => $original . ' -> ' . $description)
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed')
|
||||
->title(trans('server/setting.server_info.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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
|
||||
class ListAllocations extends ListRecords
|
||||
{
|
||||
@@ -29,8 +30,10 @@ class ListAllocations extends ListRecords
|
||||
|
||||
return [
|
||||
Action::make('addAllocation')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'tabler-network-off' : 'tabler-network')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
|
||||
->label(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')
|
||||
@@ -53,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,93 @@ 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(trans('server/backup.actions.rename.title'))
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label(trans('server/backup.actions.rename.new_name'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default(fn (Backup $backup) => $backup->name),
|
||||
])
|
||||
->action(function (Backup $backup, $data) {
|
||||
$oldName = $backup->name;
|
||||
$newName = $data['name'];
|
||||
|
||||
$backup->update(['name' => $newName]);
|
||||
|
||||
if ($oldName !== $newName) {
|
||||
Activity::event('server:backup.rename')
|
||||
->subject($backup)
|
||||
->property(['old_name' => $oldName, 'new_name' => $newName])
|
||||
->log();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/backup.actions.rename.notification_success'))
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->visible(fn (Backup $backup) => $backup->status === BackupStatus::Successful),
|
||||
Action::make('lock')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock' : 'tabler-lock-open')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server))
|
||||
->label(fn (Backup $backup) => !$backup->is_locked ? '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 +208,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 +266,9 @@ class BackupResource extends Resource
|
||||
'index' => Pages\ListBackups::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/backup.title');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use Filament\Actions\CreateAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class ListBackups extends ListRecords
|
||||
@@ -33,8 +34,9 @@ class ListBackups extends ListRecords
|
||||
return [
|
||||
CreateAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_BACKUP_CREATE, $server))
|
||||
->label(fn () => $server->backups()->count() >= $server->backup_limit ? 'Backup limit reached' : 'Create Backup')
|
||||
->icon('tabler-file-zip')->iconButton()->iconSize(IconSize::Large)
|
||||
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
|
||||
->tooltip(fn () => $server->backups()->count() >= $server->backup_limit ? trans('server/backup.actions.create.limit') : trans('server/backup.actions.create.title'))
|
||||
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
|
||||
->createAnother(false)
|
||||
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
|
||||
@@ -53,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();
|
||||
}
|
||||
}),
|
||||
@@ -72,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,17 +105,22 @@ 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([
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||
->modalHeading(fn (Database $database) => trans('server/database.viewing', ['database' => $database->database])),
|
||||
DeleteAction::make()
|
||||
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
|
||||
]);
|
||||
@@ -148,4 +158,9 @@ class DatabaseResource extends Resource
|
||||
'index' => Pages\ListDatabases::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/database.title');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
|
||||
class ListDatabases extends ListRecords
|
||||
{
|
||||
@@ -32,7 +33,9 @@ class ListDatabases extends ListRecords
|
||||
|
||||
return [
|
||||
CreateAction::make('new')
|
||||
->label(fn () => $server->databases()->count() >= $server->database_limit ? 'Database limit reached' : 'Create Database')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon(fn () => $server->databases()->count() >= $server->database_limit ? 'tabler-database-x' : 'tabler-database-plus')
|
||||
->tooltip(fn () => $server->databases()->count() >= $server->database_limit ? trans('server/database.limit') : trans('server/database.create_database'))
|
||||
->disabled(fn () => $server->databases()->count() >= $server->database_limit)
|
||||
->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary')
|
||||
->createAnother(false)
|
||||
@@ -41,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('%'),
|
||||
]),
|
||||
])
|
||||
@@ -73,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,25 +133,25 @@ class EditFiles extends Page
|
||||
try {
|
||||
return $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
|
||||
} catch (FileSizeTooLargeException) {
|
||||
AlertBanner::make()
|
||||
->title('<code>' . basename($this->path) . '</code> is too large!')
|
||||
->body('Max is ' . convert_bytes_to_readable(config('panel.files.max_edit_size')))
|
||||
AlertBanner::make('file_too_large')
|
||||
->title(trans('server/file.alerts.file_too_large.title', ['name' => basename($this->path)]))
|
||||
->body(trans('server/file.alerts.file_too_large.body', ['max' => convert_bytes_to_readable(config('panel.files.max_edit_size'))]))
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
} catch (FileNotFoundException) {
|
||||
AlertBanner::make()
|
||||
->title('<code>' . basename($this->path) . '</code> not found!')
|
||||
AlertBanner::make('file_not_found')
|
||||
->title(trans('server/file.alerts.file_not_found.title', ['name' => basename($this->path)]))
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
$this->redirect(ListFiles::getUrl(['path' => dirname($this->path)]));
|
||||
} catch (FileNotEditableException) {
|
||||
AlertBanner::make()
|
||||
->title('<code>' . basename($this->path) . '</code> is a directory')
|
||||
AlertBanner::make('file_is_directory')
|
||||
->title(trans('server/file.alerts.file_not_found.title', ['name' => basename($this->path)]))
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
@@ -179,20 +179,11 @@ class EditFiles extends Page
|
||||
|
||||
if (str($path)->endsWith('.pelicanignore')) {
|
||||
AlertBanner::make('.pelicanignore_info')
|
||||
->title('You\'re editing a <code>.pelicanignore</code> file!')
|
||||
->body('Any files or directories listed in here will be excluded from backups. Wildcards are supported by using an asterisk (<code>*</code>).<br>You can negate a prior rule by prepending an exclamation point (<code>!</code>).')
|
||||
->title(trans('server/file.alerts.pelicanignore.title'))
|
||||
->body(trans('server/file.alerts.pelicanignore.body'))
|
||||
->info()
|
||||
->closable()
|
||||
->send();
|
||||
|
||||
try {
|
||||
$this->getDaemonFileRepository()->getDirectory('/');
|
||||
} catch (ConnectionException) {
|
||||
AlertBanner::make('node_connection_error')
|
||||
->title('Could not connect to the node!')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ use Filament\Notifications\Notification;
|
||||
use Filament\Panel;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
@@ -89,49 +90,52 @@ class ListFiles extends ListRecords
|
||||
->defaultSort('name')
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(trans('server/file.name'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->icon(fn (File $file) => $file->getIcon()),
|
||||
BytesColumn::make('size')
|
||||
->label(trans('server/file.size'))
|
||||
->visibleFrom('md')
|
||||
->state(fn (File $file) => $file->is_directory ? null : $file->size)
|
||||
->sortable(),
|
||||
DateTimeColumn::make('modified_at')
|
||||
->label(trans('server/file.modified_at'))
|
||||
->visibleFrom('md')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->recordUrl(function (File $file) use ($server) {
|
||||
if ($file->is_directory) {
|
||||
return self::getUrl(['path' => join_paths($this->path, $file->name)]);
|
||||
return self::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]);
|
||||
}
|
||||
|
||||
if (!auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
|
||||
return $file->canEdit() ? EditFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]) : null;
|
||||
})
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->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)])),
|
||||
->url(fn (File $file) => self::getUrl(['path' => encode_path(join_paths($this->path, $file->name))])),
|
||||
EditAction::make('edit')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (File $file) => $file->canEdit())
|
||||
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
->url(fn (File $file) => EditFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))])),
|
||||
ActionGroup::make([
|
||||
Action::make('rename')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->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.file_name'))
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required(),
|
||||
])
|
||||
@@ -148,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) {
|
||||
@@ -166,7 +170,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File copied')
|
||||
->title(trans('server/file.actions.copy.notification'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@@ -174,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),
|
||||
->url(fn (File $file) => DownloadFiles::getUrl(['path' => encode_path(join_paths($this->path, $file->name))]), true),
|
||||
Action::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('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')
|
||||
@@ -208,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);
|
||||
@@ -231,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);
|
||||
@@ -243,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);
|
||||
@@ -265,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'),
|
||||
])
|
||||
@@ -289,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();
|
||||
@@ -298,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) {
|
||||
@@ -310,7 +320,7 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Unarchive completed')
|
||||
->title(trans('server/file.actions.unarchive.notification'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@@ -338,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')
|
||||
@@ -357,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();
|
||||
}),
|
||||
@@ -365,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'),
|
||||
])
|
||||
@@ -381,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();
|
||||
@@ -400,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();
|
||||
}),
|
||||
@@ -416,10 +426,10 @@ class ListFiles extends ListRecords
|
||||
return [
|
||||
HeaderAction::make('new_file')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('New File')
|
||||
->color('gray')
|
||||
->keyBindings('')
|
||||
->modalSubmitActionLabel('Create')
|
||||
->tooltip(trans('server/file.actions.new_file.title'))
|
||||
->hiddenLabel()->icon('tabler-file-plus')->iconButton()->iconSize(IconSize::Large)
|
||||
->color('primary')
|
||||
->modalSubmitActionLabel(trans('server/file.actions.new_file.create'))
|
||||
->action(function ($data) {
|
||||
$path = join_paths($this->path, $data['name']);
|
||||
try {
|
||||
@@ -429,8 +439,8 @@ class ListFiles extends ListRecords
|
||||
->property('file', join_paths($path, $data['name']))
|
||||
->log();
|
||||
} catch (FileExistsException) {
|
||||
AlertBanner::make()
|
||||
->title('<code>' . $path . '</code> already exists!')
|
||||
AlertBanner::make('file_already_exists')
|
||||
->title(trans('server/file.alerts.file_already_exists.title', ['name' => $path]))
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
@@ -440,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()
|
||||
@@ -458,8 +468,9 @@ class ListFiles extends ListRecords
|
||||
]),
|
||||
HeaderAction::make('new_folder')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('New Folder')
|
||||
->color('gray')
|
||||
->hiddenLabel()->icon('tabler-folder-plus')->iconButton()->iconSize(IconSize::Large)
|
||||
->tooltip(trans('server/file.actions.new_folder.title'))
|
||||
->color('primary')
|
||||
->action(function ($data) {
|
||||
try {
|
||||
$this->getDaemonFileRepository()->createDirectory($data['name'], $this->path);
|
||||
@@ -469,8 +480,8 @@ class ListFiles extends ListRecords
|
||||
->log();
|
||||
} catch (FileExistsException) {
|
||||
$path = join_paths($this->path, $data['name']);
|
||||
AlertBanner::make()
|
||||
->title('<code>' . $path . '</code> already exists!')
|
||||
AlertBanner::make('folder_already_exists')
|
||||
->title(trans('server/file.alerts.file_already_exists.title', ['name' => $path]))
|
||||
->danger()
|
||||
->closable()
|
||||
->send();
|
||||
@@ -480,12 +491,14 @@ 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))
|
||||
->label('Upload')
|
||||
->hiddenLabel()->icon('tabler-upload')->iconButton()->iconSize(IconSize::Large)
|
||||
->tooltip(trans('server/file.actions.upload.title'))
|
||||
->color('success')
|
||||
->action(function ($data) {
|
||||
if (count($data['files']) > 0 && !isset($data['url'])) {
|
||||
/** @var UploadedFile $file */
|
||||
@@ -512,7 +525,8 @@ class ListFiles extends ListRecords
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->schema([
|
||||
Tab::make('Upload Files')
|
||||
Tab::make('from_files')
|
||||
->label(trans('server/file.actions.upload.from_files'))
|
||||
->live()
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
@@ -522,23 +536,29 @@ 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('url')
|
||||
->label(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(),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
HeaderAction::make('search')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->label('Global Search')
|
||||
->modalSubmitActionLabel('Search')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->tooltip(trans('server/file.actions.global_search.title'))
|
||||
->color('primary')
|
||||
->icon('tabler-world-search')
|
||||
->modalHeading(trans('server/file.actions.global_search.title'))
|
||||
->modalSubmitActionLabel(trans('server/file.actions.global_search.search'))
|
||||
->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),
|
||||
@@ -586,4 +606,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_for_term', ['term' => ' "' . $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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
|
||||
class EditSchedule extends EditRecord
|
||||
{
|
||||
@@ -46,14 +47,26 @@ class EditSchedule extends EditRecord
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-trash')
|
||||
->tooltip(trans('server/schedule.delete'))
|
||||
->after(function ($record) {
|
||||
Activity::event('server:schedule.delete')
|
||||
->property('name', $record->name)
|
||||
->log();
|
||||
}),
|
||||
ExportScheduleAction::make(),
|
||||
$this->getSaveFormAction()->formId('form')->label('Save'),
|
||||
$this->getCancelFormAction()->formId('form'),
|
||||
ExportScheduleAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-download')
|
||||
->tooltip(trans('server/schedule.export')),
|
||||
$this->getSaveFormAction()->formId('form')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-device-floppy')
|
||||
->tooltip(trans('server/schedule.save')),
|
||||
$this->getCancelFormAction()->formId('form')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-cancel')
|
||||
->tooltip(trans('server/schedule.cancel')),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
|
||||
class ListSchedules extends ListRecords
|
||||
{
|
||||
@@ -23,8 +24,13 @@ class ListSchedules extends ListRecords
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('New Schedule'),
|
||||
ImportScheduleAction::make(),
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-calendar-plus')
|
||||
->tooltip(trans('server/schedule.new')),
|
||||
ImportScheduleAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-download')
|
||||
->tooltip(trans('server/schedule.import')),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -32,4 +38,9 @@ class ListSchedules extends ListRecords
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/schedule.title');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
|
||||
class ViewSchedule extends ViewRecord
|
||||
{
|
||||
@@ -28,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) {
|
||||
@@ -41,7 +42,10 @@ class ViewSchedule extends ViewRecord
|
||||
|
||||
$this->fillForm();
|
||||
}),
|
||||
EditAction::make(),
|
||||
EditAction::make()
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-calendar-code')
|
||||
->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,15 @@ 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())
|
||||
$tabs[] = Tab::make($data['name'])
|
||||
->label(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 +122,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 +162,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 +189,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 +235,9 @@ class UserResource extends Resource
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/user.title');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
@@ -47,14 +49,15 @@ 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())
|
||||
$tabs[] = Tab::make($data['name'])
|
||||
->label(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'])
|
||||
@@ -69,7 +72,9 @@ class ListUsers extends ListRecords
|
||||
|
||||
return [
|
||||
Actions\CreateAction::make('invite')
|
||||
->label('Invite User')
|
||||
->hiddenLabel()->iconButton()->iconSize(IconSize::Large)
|
||||
->icon('tabler-user-plus')
|
||||
->tooltip(trans('server/user.invite_user'))
|
||||
->createAnother(false)
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_CREATE, $server))
|
||||
->form([
|
||||
@@ -83,6 +88,7 @@ class ListUsers extends ListRecords
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('email')
|
||||
->label(trans('server/user.email'))
|
||||
->email()
|
||||
->inlineLabel()
|
||||
->columnSpan([
|
||||
@@ -94,7 +100,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) {
|
||||
@@ -114,8 +120,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']);
|
||||
|
||||
@@ -137,12 +143,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();
|
||||
@@ -157,4 +163,9 @@ class ListUsers extends ListRecords
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return trans('server/user.title');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,8 +133,8 @@ class ServerConsole extends Widget
|
||||
public function websocketError(): void
|
||||
{
|
||||
AlertBanner::make('websocket_error')
|
||||
->title('Could not connect to websocket!')
|
||||
->body('Check your browser console for more details.')
|
||||
->title(trans('server/console.websocket_error.title'))
|
||||
->body(trans('server/console.websocket_error.body'))
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use Carbon\Carbon;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class ServerCpuChart extends ChartWidget
|
||||
{
|
||||
@@ -31,7 +30,7 @@ class ServerCpuChart extends ChartWidget
|
||||
$cpu = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))
|
||||
->slice(-$period)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => Number::format($value, maxPrecision: 2),
|
||||
'cpu' => round($value, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
@@ -80,6 +79,6 @@ class ServerCpuChart extends ChartWidget
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return 'CPU';
|
||||
return trans('server/console.labels.cpu');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use Carbon\Carbon;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class ServerMemoryChart extends ChartWidget
|
||||
{
|
||||
@@ -31,7 +30,7 @@ class ServerMemoryChart extends ChartWidget
|
||||
$memUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))
|
||||
->slice(-$period)
|
||||
->map(fn ($value, $key) => [
|
||||
'memory' => Number::format(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, maxPrecision: 2),
|
||||
'memory' => round(config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, auth()->user()->timezone ?? 'UTC')->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
@@ -80,6 +79,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Models\Server;
|
||||
use Carbon\CarbonInterface;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Widgets\StatsOverviewWidget;
|
||||
use Illuminate\Support\Number;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
class ServerOverview extends StatsOverviewWidget
|
||||
@@ -20,14 +19,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()),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -54,9 +53,9 @@ class ServerOverview extends StatsOverviewWidget
|
||||
}
|
||||
|
||||
$data = collect(cache()->get("servers.{$this->server->id}.cpu_absolute"))->last(default: 0);
|
||||
$cpu = Number::format($data, maxPrecision: 2, locale: auth()->user()->language) . ' %';
|
||||
$cpu = format_number($data, maxPrecision: 2) . ' %';
|
||||
|
||||
return $cpu . ($this->server->cpu > 0 ? ' / ' . Number::format($this->server->cpu, locale: auth()->user()->language) . ' %' : ' / ∞');
|
||||
return $cpu . ($this->server->cpu > 0 ? ' / ' . format_number($this->server->cpu) . ' %' : ' / ∞');
|
||||
}
|
||||
|
||||
public function memoryUsage(): string
|
||||
@@ -68,7 +67,7 @@ class ServerOverview extends StatsOverviewWidget
|
||||
}
|
||||
|
||||
$latestMemoryUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->last(default: 0);
|
||||
$totalMemory = $this->server->memory * 2 ** 20;
|
||||
$totalMemory = $this->server->memory * (config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000);
|
||||
|
||||
$used = convert_bytes_to_readable($latestMemoryUsed);
|
||||
$total = convert_bytes_to_readable($totalMemory);
|
||||
@@ -98,7 +97,7 @@ class ServerOverview extends StatsOverviewWidget
|
||||
$this->js("window.navigator.clipboard.writeText('{$value}');");
|
||||
|
||||
Notification::make()
|
||||
->title('Copied to clipboard')
|
||||
->title(trans('server/dashboard.copied'))
|
||||
->body($value)
|
||||
->success()
|
||||
->send();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -16,12 +16,12 @@ class ServerInstalledListener
|
||||
|
||||
Notification::make()
|
||||
->status($event->successful ? 'success' : 'danger')
|
||||
->title('Server ' . ($event->initialInstall ? 'Installation' : 'Reinstallation') . ' ' . ($event->successful ? 'completed' : 'failed'))
|
||||
->body('Server Name: ' . $event->server->name)
|
||||
->title(trans('notifications.' . ($event->initialInstall ? 'installation' : 'reinstallation') . '_' . ($event->successful ? 'completed' : 'failed')))
|
||||
->body(trans('server/setting.server_info.server_name', ['name' => $event->server->name]))
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->button()
|
||||
->label('Open Server')
|
||||
->label(trans('notifications.open_server'))
|
||||
->markAsRead()
|
||||
->url(fn () => Console::getUrl(panel: 'server', tenant: $event->server)),
|
||||
])
|
||||
|
||||
@@ -16,12 +16,12 @@ class SubUserAddedListener
|
||||
$event->subuser->loadMissing('user');
|
||||
|
||||
Notification::make()
|
||||
->title('Added to Server')
|
||||
->body('You have been added as a subuser to ' . $event->subuser->server->name . '.')
|
||||
->title(trans('notifications.user_added.title'))
|
||||
->body(trans('notifications.user_added.body', ['server' => $event->subuser->server->name]))
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->button()
|
||||
->label('Open Server')
|
||||
->label(trans('notifications.open_server'))
|
||||
->markAsRead()
|
||||
->url(fn () => Console::getUrl(panel: 'server', tenant: $event->subuser->server)),
|
||||
])
|
||||
|
||||
@@ -11,8 +11,8 @@ class SubUserRemovedListener
|
||||
public function handle(SubUserRemoved $event): void
|
||||
{
|
||||
Notification::make()
|
||||
->title('Removed from Server')
|
||||
->body('You have been removed as a subuser from ' . $event->server->name . '.')
|
||||
->title(trans('notifications.user_removed.title'))
|
||||
->body(trans('notifications.user_removed.body', ['server' => $event->server->name]))
|
||||
->sendToDatabase($event->user);
|
||||
|
||||
$event->user->notify(new RemovedFromServer($event->server));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Livewire\Installer;
|
||||
|
||||
use App\Filament\Admin\Pages\Dashboard;
|
||||
use App\Livewire\Installer\Steps\CacheStep;
|
||||
use App\Livewire\Installer\Steps\DatabaseStep;
|
||||
use App\Livewire\Installer\Steps\EnvironmentStep;
|
||||
@@ -10,11 +9,16 @@ use App\Livewire\Installer\Steps\QueueStep;
|
||||
use App\Livewire\Installer\Steps\RequirementsStep;
|
||||
use App\Livewire\Installer\Steps\SessionStep;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\LanguageService;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\CheckMigrationsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Wizard;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
@@ -23,6 +27,7 @@ use Filament\Notifications\Notification;
|
||||
use Filament\Pages\SimplePage;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
@@ -41,6 +46,11 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
|
||||
protected static string $view = 'filament.pages.installer';
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('installer.title');
|
||||
}
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
@@ -61,6 +71,10 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Grid::make()
|
||||
->schema([
|
||||
$this->getLanguageComponent(),
|
||||
]),
|
||||
Wizard::make([
|
||||
RequirementsStep::make(),
|
||||
EnvironmentStep::make($this),
|
||||
@@ -70,20 +84,40 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
SessionStep::make(),
|
||||
])
|
||||
->persistStepInQueryString()
|
||||
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
|
||||
->nextAction(function (Action $action) {
|
||||
$action
|
||||
->label(trans('installer.next_step'))
|
||||
->keyBindings('enter');
|
||||
})
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
size="sm"
|
||||
wire:loading.attr="disabled"
|
||||
>
|
||||
Finish
|
||||
{{ trans('installer.finish') }}
|
||||
<span wire:loading><x-filament::loading-indicator class="h-4 w-4" /></span>
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getLanguageComponent(): Component
|
||||
{
|
||||
return Select::make('language')
|
||||
->hiddenLabel()
|
||||
->prefix(trans('profile.language'))
|
||||
->prefixIcon('tabler-flag')
|
||||
->required()
|
||||
->live()
|
||||
->default('en')
|
||||
->selectablePlaceholder(false)
|
||||
->options(fn (LanguageService $languageService) => $languageService->getAvailableLanguages())
|
||||
->afterStateUpdated(fn ($state, Application $app) => $app->setLocale($state ?? config('app.locale')))
|
||||
->native(false)
|
||||
->columnStart(4);
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
@@ -106,7 +140,7 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
$this->writeToEnv('env_session');
|
||||
|
||||
// Redirect to admin panel
|
||||
$this->redirect(Dashboard::getUrl());
|
||||
$this->redirect(Filament::getPanel('admin')->getUrl());
|
||||
} catch (Halt) {
|
||||
}
|
||||
}
|
||||
@@ -121,13 +155,13 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Could not write to .env file')
|
||||
->title(trans('installer.exceptions.write_env'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while writing .env file');
|
||||
throw new Halt(trans('installer.exceptions.write_env'));
|
||||
}
|
||||
|
||||
Artisan::call('config:clear');
|
||||
@@ -144,23 +178,23 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Migrations failed')
|
||||
->title(trans('installer.database.exceptions.migration'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while running migrations');
|
||||
throw new Halt(trans('installer.exceptions.migration'));
|
||||
}
|
||||
|
||||
if (!$this->hasCompletedMigrations()) {
|
||||
Notification::make()
|
||||
->title('Migrations failed')
|
||||
->title(trans('installer.database.exceptions.migration'))
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Migrations failed');
|
||||
throw new Halt(trans('installer.database.exceptions.migration'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,13 +209,13 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Could not create admin user')
|
||||
->title(trans('installer.exceptions.create_user'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while creating admin user');
|
||||
throw new Halt(trans('installer.exceptions.create_user'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,13 @@ class CacheStep
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('cache')
|
||||
->label('Cache')
|
||||
->label(trans('installer.cache.title'))
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_cache.CACHE_STORE')
|
||||
->label('Cache Driver')
|
||||
->label(trans('installer.cache.driver'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
|
||||
->hintIconTooltip(trans('installer.cache.driver_help'))
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::CACHE_DRIVERS)
|
||||
@@ -50,31 +50,31 @@ class CacheStep
|
||||
}
|
||||
}),
|
||||
TextInput::make('env_cache.REDIS_HOST')
|
||||
->label('Redis Host')
|
||||
->label(trans('installer.cache.fields.host'))
|
||||
->placeholder('127.0.0.1')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
|
||||
->hintIconTooltip(trans('installer.cache.fields.host_help'))
|
||||
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.host') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_PORT')
|
||||
->label('Redis Port')
|
||||
->label(trans('installer.cache.fields.port'))
|
||||
->placeholder('6379')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your redis server.')
|
||||
->hintIconTooltip(trans('installer.cache.fields.port_help'))
|
||||
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.port') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_USERNAME')
|
||||
->label('Redis Username')
|
||||
->label(trans('installer.cache.fields.username'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your redis user. Can be empty')
|
||||
->hintIconTooltip(trans('installer.cache.fields.username_help'))
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.username') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_PASSWORD')
|
||||
->label('Redis Password')
|
||||
->label(trans('installer.cache.fields.password'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password for your redis user. Can be empty.')
|
||||
->hintIconTooltip(trans('installer.cache.fields.password_help'))
|
||||
->password()
|
||||
->revealable()
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.password') : null)
|
||||
@@ -110,7 +110,7 @@ class CacheStep
|
||||
$redis->connection()->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->title(trans('installer.cache.exception'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@@ -25,13 +25,13 @@ class DatabaseStep
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('database')
|
||||
->label('Database')
|
||||
->label(trans('installer.database.title'))
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_database.DB_CONNECTION')
|
||||
->label('Database Driver')
|
||||
->label(trans('installer.database.driver'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
|
||||
->hintIconTooltip(trans('installer.database.driver_help'))
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::DATABASE_DRIVERS)
|
||||
@@ -61,40 +61,40 @@ class DatabaseStep
|
||||
}
|
||||
}),
|
||||
TextInput::make('env_database.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? trans('installer.database.fields.path') : trans('installer.database.fields.name'))
|
||||
->placeholder(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
|
||||
->hintIconTooltip(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? trans('installer.database.fields.path_help') : trans('installer.database.fields.name_help'))
|
||||
->required()
|
||||
->default('database.sqlite'),
|
||||
TextInput::make('env_database.DB_HOST')
|
||||
->label('Database Host')
|
||||
->label(trans('installer.database.fields.host'))
|
||||
->placeholder('127.0.0.1')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your database. Make sure it is reachable.')
|
||||
->hintIconTooltip(trans('installer.database.fields.host_help'))
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PORT')
|
||||
->label('Database Port')
|
||||
->label(trans('installer.database.fields.port'))
|
||||
->placeholder('3306')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your database.')
|
||||
->hintIconTooltip(trans('installer.database.fields.port_help'))
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_USERNAME')
|
||||
->label('Database Username')
|
||||
->label(trans('installer.database.fields.username'))
|
||||
->placeholder('pelican')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your database user.')
|
||||
->hintIconTooltip(trans('installer.database.fields.username_help'))
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PASSWORD')
|
||||
->label('Database Password')
|
||||
->label(trans('installer.database.fields.password'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password of your database user. Can be empty.')
|
||||
->hintIconTooltip(trans('installer.database.fields.password_help'))
|
||||
->password()
|
||||
->revealable()
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
@@ -103,7 +103,7 @@ class DatabaseStep
|
||||
$driver = $get('env_database.DB_CONNECTION');
|
||||
|
||||
if (!self::testConnection($driver, $get('env_database.DB_HOST'), $get('env_database.DB_PORT'), $get('env_database.DB_DATABASE'), $get('env_database.DB_USERNAME'), $get('env_database.DB_PASSWORD'))) {
|
||||
throw new Halt('Database connection failed');
|
||||
throw new Halt(trans('installer.database.exceptions.connection'));
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_database');
|
||||
@@ -133,7 +133,7 @@ class DatabaseStep
|
||||
DB::disconnect('_panel_install_test');
|
||||
|
||||
Notification::make()
|
||||
->title('Database connection failed')
|
||||
->title(trans('installer.database.exceptions.connection'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@@ -12,36 +12,36 @@ class EnvironmentStep
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('environment')
|
||||
->label('Environment')
|
||||
->label(trans('installer.environment.title'))
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env_general.APP_NAME')
|
||||
->label('App Name')
|
||||
->label(trans('installer.environment.fields.app_name'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the Name of your Panel.')
|
||||
->hintIconTooltip(trans('installer.environment.fields.app_name_help'))
|
||||
->required()
|
||||
->default(config('app.name')),
|
||||
TextInput::make('env_general.APP_URL')
|
||||
->label('App URL')
|
||||
->label(trans('installer.environment.fields.app_url'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the URL you access your Panel from.')
|
||||
->hintIconTooltip(trans('installer.environment.fields.app_url_help'))
|
||||
->required()
|
||||
->default(url('')),
|
||||
Fieldset::make('adminuser')
|
||||
->label('Admin User')
|
||||
Fieldset::make('admin_user')
|
||||
->label(trans('installer.environment.fields.account.section'))
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextInput::make('user.email')
|
||||
->label('E-Mail')
|
||||
->label(trans('installer.environment.fields.account.email'))
|
||||
->required()
|
||||
->email()
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Username')
|
||||
->label(trans('installer.environment.fields.account.username'))
|
||||
->required()
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Password')
|
||||
->label(trans('installer.environment.fields.account.password'))
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
|
||||
@@ -22,35 +22,35 @@ class QueueStep
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('queue')
|
||||
->label('Queue')
|
||||
->label(trans('installer.queue.title'))
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_queue.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->label(trans('installer.queue.driver'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
|
||||
->hintIconTooltip(trans('installer.queue.driver_help'))
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::QUEUE_DRIVERS)
|
||||
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
|
||||
->default(config('queue.default')),
|
||||
Toggle::make('done')
|
||||
->label('I have done both steps below.')
|
||||
->label(trans('installer.queue.fields.done'))
|
||||
->accepted(fn () => !@file_exists('/.dockerenv'))
|
||||
->inline(false)
|
||||
->validationMessages([
|
||||
'accepted' => 'You need to do both steps before continuing!',
|
||||
'accepted' => trans('installer.queue.fields.done_validation'),
|
||||
])
|
||||
->hidden(fn () => @file_exists('/.dockerenv')),
|
||||
TextInput::make('crontab')
|
||||
->label(new HtmlString('Run the following command to set up your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
|
||||
->label(new HtmlString(trans('installer.queue.fields.crontab')))
|
||||
->disabled()
|
||||
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
|
||||
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -')
|
||||
->hidden(fn () => @file_exists('/.dockerenv'))
|
||||
->columnSpanFull(),
|
||||
TextInput::make('queueService')
|
||||
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
|
||||
->label(new HtmlString(trans('installer.queue.fields.service')))
|
||||
->disabled()
|
||||
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
|
||||
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service')
|
||||
|
||||
@@ -18,13 +18,13 @@ class RequirementsStep
|
||||
$correctPhpVersion = $compare >= 0;
|
||||
|
||||
$fields = [
|
||||
Section::make('PHP Version')
|
||||
->description(self::MIN_PHP_VERSION . ' or newer')
|
||||
Section::make(trans('installer.requirements.sections.version.title'))
|
||||
->description(trans('installer.requirements.sections.version.or_newer', ['version' => self::MIN_PHP_VERSION]))
|
||||
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctPhpVersion ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('Your PHP Version is ' . PHP_VERSION . '.'),
|
||||
->content(trans('installer.requirements.sections.version.content', ['version' => PHP_VERSION])),
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -41,16 +41,16 @@ class RequirementsStep
|
||||
];
|
||||
$allExtensionsInstalled = !in_array(false, $phpExtensions);
|
||||
|
||||
$fields[] = Section::make('PHP Extensions')
|
||||
$fields[] = Section::make(trans('installer.requirements.sections.extensions.title'))
|
||||
->description(implode(', ', array_keys($phpExtensions)))
|
||||
->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($allExtensionsInstalled ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All needed PHP Extensions are installed.')
|
||||
->content(trans('installer.requirements.sections.extensions.good'))
|
||||
->visible($allExtensionsInstalled),
|
||||
Placeholder::make('')
|
||||
->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false)))
|
||||
->content(trans('installer.requirements.sections.extensions.bad', ['extensions' => implode(', ', array_keys($phpExtensions, false))]))
|
||||
->visible(!$allExtensionsInstalled),
|
||||
]);
|
||||
|
||||
@@ -60,30 +60,30 @@ class RequirementsStep
|
||||
];
|
||||
$correctFolderPermissions = !in_array(false, $folderPermissions);
|
||||
|
||||
$fields[] = Section::make('Folder Permissions')
|
||||
$fields[] = Section::make(trans('installer.requirements.sections.permissions.title'))
|
||||
->description(implode(', ', array_keys($folderPermissions)))
|
||||
->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctFolderPermissions ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All Folders have the correct permissions.')
|
||||
->content(trans('installer.requirements.sections.permissions.good'))
|
||||
->visible($correctFolderPermissions),
|
||||
Placeholder::make('')
|
||||
->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false)))
|
||||
->content(trans('installer.requirements.sections.permissions.bad', ['folders' => implode(', ', array_keys($folderPermissions, false))]))
|
||||
->visible(!$correctFolderPermissions),
|
||||
]);
|
||||
|
||||
return Step::make('requirements')
|
||||
->label('Server Requirements')
|
||||
->label(trans('installer.requirements.title'))
|
||||
->schema($fields)
|
||||
->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) {
|
||||
if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) {
|
||||
Notification::make()
|
||||
->title('Some requirements are missing!')
|
||||
->title(trans('installer.requirements.exception'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt('Some requirements are missing');
|
||||
throw new Halt(trans('installer.requirements.title'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ class SessionStep
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('session')
|
||||
->label('Session')
|
||||
->label(trans('installer.session.title'))
|
||||
->schema([
|
||||
ToggleButtons::make('env_session.SESSION_DRIVER')
|
||||
->label('Session Driver')
|
||||
->label(trans('installer.session.driver'))
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
|
||||
->hintIconTooltip(trans('installer.session.driver_help'))
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::SESSION_DRIVERS)
|
||||
|
||||
@@ -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-md font-semibold">{{ Number::format(0, precision: 2, locale: auth()->user()->language ?? 'en') . '%' }}</p>
|
||||
<p class="text-sm dark:text-gray-400">{{ trans('server/dashboard.cpu') }}</p>
|
||||
<p class="text-md font-semibold">{{ format_number(0, precision: 2) . '%' }}</p>
|
||||
<hr class="p-0.5">
|
||||
<p class="text-xs dark:text-gray-400">{{ $server->formatResource('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>
|
||||
|
||||
@@ -21,7 +21,6 @@ use Illuminate\Support\Str;
|
||||
* \App\Models\ActivityLog.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $batch
|
||||
* @property string $event
|
||||
* @property string $ip
|
||||
* @property string|null $description
|
||||
@@ -43,7 +42,6 @@ use Illuminate\Support\Str;
|
||||
* @method static Builder|ActivityLog whereActorId($value)
|
||||
* @method static Builder|ActivityLog whereActorType($value)
|
||||
* @method static Builder|ActivityLog whereApiKeyId($value)
|
||||
* @method static Builder|ActivityLog whereBatch($value)
|
||||
* @method static Builder|ActivityLog whereDescription($value)
|
||||
* @method static Builder|ActivityLog whereEvent($value)
|
||||
* @method static Builder|ActivityLog whereId($value)
|
||||
@@ -76,7 +74,6 @@ class ActivityLog extends Model implements HasIcon, HasLabel
|
||||
/** @var array<array-key, string[]> */
|
||||
public static array $validationRules = [
|
||||
'event' => ['required', 'string'],
|
||||
'batch' => ['nullable', 'uuid'],
|
||||
'ip' => ['required', 'string'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'properties' => ['array'],
|
||||
|
||||
@@ -216,7 +216,7 @@ class ApiKey extends PersonalAccessToken
|
||||
{
|
||||
Assert::oneOf($type, [self::TYPE_ACCOUNT, self::TYPE_APPLICATION]);
|
||||
|
||||
return $type === self::TYPE_ACCOUNT ? 'plcn_' : 'peli_';
|
||||
return $type === self::TYPE_ACCOUNT ? 'pacc_' : 'papp_';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,8 +194,8 @@ class File extends Model
|
||||
$message = str('Node connection failed');
|
||||
}
|
||||
|
||||
AlertBanner::make()
|
||||
->title('Could not load files!')
|
||||
AlertBanner::make('files_node_error')
|
||||
->title(trans('server/file.alerts.files_node_error.title'))
|
||||
->body($message->toString())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@@ -44,7 +44,7 @@ class Role extends BaseRole
|
||||
'health' => [
|
||||
'view',
|
||||
],
|
||||
'activity' => [
|
||||
'activityLog' => [
|
||||
'seeIps',
|
||||
],
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user