mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
1 Commits
release/v1
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f240ac00f |
6
.github/docker/entrypoint.sh
vendored
6
.github/docker/entrypoint.sh
vendored
@@ -52,11 +52,11 @@ crond -L /var/log/crond -l 5
|
||||
export SUPERVISORD_CADDY=false
|
||||
|
||||
## disable caddy if SKIP_CADDY is set
|
||||
if [[ "${SKIP_CADDY:-}" == "true" ]]; then
|
||||
echo "Starting PHP-FPM only"
|
||||
else
|
||||
if [[ -z $SKIP_CADDY ]]; then
|
||||
echo "Starting PHP-FPM and Caddy"
|
||||
export SUPERVISORD_CADDY=true
|
||||
else
|
||||
echo "Starting PHP-FPM only"
|
||||
fi
|
||||
|
||||
chown -R www-data:www-data /pelican-data/.env /pelican-data/database
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -87,7 +87,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
|
||||
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
|
||||
@@ -26,8 +26,8 @@ class InfoCommand extends Command
|
||||
{
|
||||
$this->output->title('Version Information');
|
||||
$this->table([], [
|
||||
['Panel Version', $this->versionService->currentPanelVersion()],
|
||||
['Latest Version', $this->versionService->latestPanelVersion()],
|
||||
['Panel Version', $this->versionService->versionData()['version']],
|
||||
['Latest Version', $this->versionService->getPanel()],
|
||||
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
|
||||
], 'compact');
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ class BulkPowerActionCommand extends Command
|
||||
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$powerRepository = $this->powerRepository;
|
||||
// @phpstan-ignore-next-line
|
||||
$this->getQueryBuilder($servers, $nodes)->each(function (Server $server) use ($action, $powerRepository, &$bar) {
|
||||
$bar->clear();
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
|
||||
use App\Console\Commands\Schedule\ProcessRunnableCommand;
|
||||
use App\Jobs\NodeStatistics;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Webhook;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Database\Console\PruneCommand;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
@@ -49,9 +48,5 @@ class Kernel extends ConsoleKernel
|
||||
if (config('activity.prune_days')) {
|
||||
$schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily();
|
||||
}
|
||||
|
||||
if (config('panel.webhook.prune_days')) {
|
||||
$schedule->command(PruneCommand::class, ['--model' => [Webhook::class]])->daily();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
|
||||
enum EditorLanguages: string implements HasLabel
|
||||
{
|
||||
case plaintext = 'plaintext';
|
||||
case abap = 'abap';
|
||||
case apex = 'apex';
|
||||
case azcali = 'azcali';
|
||||
case bat = 'bat';
|
||||
case bicep = 'bicep';
|
||||
case cameligo = 'cameligo';
|
||||
case coljure = 'coljure';
|
||||
case coffeescript = 'coffeescript';
|
||||
case c = 'c';
|
||||
case cpp = 'cpp';
|
||||
case csharp = 'csharp';
|
||||
case csp = 'csp';
|
||||
case css = 'css';
|
||||
case cypher = 'cypher';
|
||||
case dart = 'dart';
|
||||
case dockerfile = 'dockerfile';
|
||||
case ecl = 'ecl';
|
||||
case elixir = 'elixir';
|
||||
case flow9 = 'flow9';
|
||||
case fsharp = 'fsharp';
|
||||
case go = 'go';
|
||||
case graphql = 'graphql';
|
||||
case handlebars = 'handlebars';
|
||||
case hcl = 'hcl';
|
||||
case html = 'html';
|
||||
case ini = 'ini';
|
||||
case java = 'java';
|
||||
case javascript = 'javascript';
|
||||
case julia = 'julia';
|
||||
case kotlin = 'kotlin';
|
||||
case less = 'less';
|
||||
case lexon = 'lexon';
|
||||
case lua = 'lua';
|
||||
case liquid = 'liquid';
|
||||
case m3 = 'm3';
|
||||
case markdown = 'markdown';
|
||||
case mdx = 'mdx';
|
||||
case mips = 'mips';
|
||||
case msdax = 'msdax';
|
||||
case mysql = 'mysql';
|
||||
case objectivec = 'objective-c';
|
||||
case pascal = 'pascal';
|
||||
case pascaligo = 'pascaligo';
|
||||
case perl = 'perl';
|
||||
case pgsql = 'pgsql';
|
||||
case php = 'php';
|
||||
case pla = 'pla';
|
||||
case postiats = 'postiats';
|
||||
case powerquery = 'powerquery';
|
||||
case powershell = 'powershell';
|
||||
case proto = 'proto';
|
||||
case pug = 'pug';
|
||||
case python = 'python';
|
||||
case qsharp = 'qsharp';
|
||||
case r = 'r';
|
||||
case razor = 'razor';
|
||||
case redis = 'redis';
|
||||
case redshift = 'redshift';
|
||||
case restructuredtext = 'restructuredtext';
|
||||
case ruby = 'ruby';
|
||||
case rust = 'rust';
|
||||
case sb = 'sb';
|
||||
case scala = 'scala';
|
||||
case scheme = 'scheme';
|
||||
case scss = 'scss';
|
||||
case shell = 'shell';
|
||||
case sol = 'sol';
|
||||
case aes = 'aes';
|
||||
case sparql = 'sparql';
|
||||
case sql = 'sql';
|
||||
case st = 'st';
|
||||
case swift = 'swift';
|
||||
case systemverilog = 'systemverilog';
|
||||
case verilog = 'verilog';
|
||||
case tcl = 'tcl';
|
||||
case twig = 'twig';
|
||||
case typescript = 'typescript';
|
||||
case typespec = 'typespec';
|
||||
case vb = 'vb';
|
||||
case wgsl = 'wgsl';
|
||||
case xml = 'xml';
|
||||
case yaml = 'yaml';
|
||||
case json = 'json';
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ActivityLogged extends Event
|
||||
{
|
||||
public function __construct(public ActivityLog $model) {}
|
||||
public function __construct(public ActivityLog $model)
|
||||
{
|
||||
}
|
||||
|
||||
public function is(string $event): bool
|
||||
{
|
||||
|
||||
@@ -7,5 +7,7 @@ use App\Events\Event;
|
||||
|
||||
class DirectLogin extends Event
|
||||
{
|
||||
public function __construct(public User $user, public bool $remember) {}
|
||||
public function __construct(public User $user, public bool $remember)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ class FailedCaptcha extends Event
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public string $ip, public ?string $message) {}
|
||||
public function __construct(public string $ip, public string $domain)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ class FailedPasswordReset extends Event
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public string $ip, public string $email) {}
|
||||
public function __construct(public string $ip, public string $email)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,7 @@ use App\Events\Event;
|
||||
|
||||
class ProvidedAuthenticationToken extends Event
|
||||
{
|
||||
public function __construct(public User $user, public bool $recovery = false) {}
|
||||
public function __construct(public User $user, public bool $recovery = false)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
abstract class Event {}
|
||||
abstract class Event
|
||||
{
|
||||
}
|
||||
|
||||
@@ -13,5 +13,7 @@ class Installed extends Event
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server) {}
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
7
app/Exceptions/AccountNotFoundException.php
Normal file
7
app/Exceptions/AccountNotFoundException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class AccountNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
7
app/Exceptions/AutoDeploymentException.php
Normal file
7
app/Exceptions/AutoDeploymentException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class AutoDeploymentException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Http\Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Container\Container;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
@@ -66,6 +67,9 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
|
||||
|
||||
return redirect()->back()->withInput();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Http\Base;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class InvalidPasswordProvidedException extends DisplayException {}
|
||||
class InvalidPasswordProvidedException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ use GuzzleHttp\Exception\GuzzleException;
|
||||
use App\Exceptions\DisplayException;
|
||||
use Illuminate\Support\Facades\Context;
|
||||
|
||||
/**
|
||||
* @method \GuzzleHttp\Exception\GuzzleException getPrevious()
|
||||
*/
|
||||
class DaemonConnectionException extends DisplayException
|
||||
{
|
||||
private int $statusCode = Response::HTTP_GATEWAY_TIMEOUT;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Http\Server;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class FileTypeNotEditableException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class PanelException extends \Exception {}
|
||||
class PanelException extends \Exception
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Repository\Daemon;
|
||||
|
||||
use App\Exceptions\Repository\RepositoryException;
|
||||
|
||||
class InvalidPowerSignalException extends RepositoryException
|
||||
{
|
||||
}
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Repository;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class DuplicateDatabaseNameException extends DisplayException {}
|
||||
class DuplicateDatabaseNameException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
9
app/Exceptions/Repository/RepositoryException.php
Normal file
9
app/Exceptions/Repository/RepositoryException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Repository;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class RepositoryException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class AllocationDoesNotBelongToServerException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class ServerUsingAllocationException extends DisplayException {}
|
||||
class ServerUsingAllocationException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Deployment;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoViableAllocationException extends DisplayException {}
|
||||
class NoViableAllocationException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
9
app/Exceptions/Service/Egg/BadJsonFormatException.php
Normal file
9
app/Exceptions/Service/Egg/BadJsonFormatException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Egg;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class BadJsonFormatException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Egg;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class HasChildrenException extends DisplayException {}
|
||||
class HasChildrenException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Egg;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoParentConfigurationFoundException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Egg\Variable;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class BadValidationRuleException extends DisplayException {}
|
||||
class BadValidationRuleException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Egg\Variable;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class ReservedVariableNameException extends DisplayException {}
|
||||
class ReservedVariableNameException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class InvalidFileUploadException extends DisplayException {}
|
||||
class InvalidFileUploadException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Node;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class ConfigurationNotPersistedException extends DisplayException {}
|
||||
class ConfigurationNotPersistedException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Schedule\Task;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class TaskIntervalTooLongException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Server;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class RequiredVariableMissingException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Subuser;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class ServerSubuserExistsException extends DisplayException {}
|
||||
class ServerSubuserExistsException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Subuser;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class UserIsServerOwnerException extends DisplayException {}
|
||||
class UserIsServerOwnerException extends DisplayException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Transformer;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class InvalidTransformerLevelException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -27,7 +27,9 @@ class BackupManager
|
||||
/**
|
||||
* BackupManager constructor.
|
||||
*/
|
||||
public function __construct(protected Application $app) {}
|
||||
public function __construct(protected Application $app)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a backup adapter instance.
|
||||
|
||||
13
app/Extensions/Facades/Theme.php
Normal file
13
app/Extensions/Facades/Theme.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Theme extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'extensions.themes';
|
||||
}
|
||||
}
|
||||
21
app/Extensions/Themes/Theme.php
Normal file
21
app/Extensions/Themes/Theme.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Themes;
|
||||
|
||||
class Theme
|
||||
{
|
||||
public function js(string $path): string
|
||||
{
|
||||
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
|
||||
}
|
||||
|
||||
public function css(string $path): string
|
||||
{
|
||||
return sprintf('<link media="all" type="text/css" rel="stylesheet" href="%s"/>' . PHP_EOL, $this->getUrl($path));
|
||||
}
|
||||
|
||||
protected function getUrl(string $path): string
|
||||
{
|
||||
return '/themes/panel/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
14
app/Facades/LogBatch.php
Normal file
14
app/Facades/LogBatch.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use App\Services\Activity\ActivityLogBatchService;
|
||||
|
||||
class LogBatch extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return ActivityLogBatchService::class;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\App\Resources;
|
||||
|
||||
use App\Filament\App\Resources\ServerResource\Pages;
|
||||
use App\Models\Server;
|
||||
use Filament\Resources\Resource;
|
||||
|
||||
class ServerResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Server::class;
|
||||
|
||||
protected static ?string $slug = '/';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListServers::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\App\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\App\Resources\ServerResource;
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Server;
|
||||
use App\Tables\Columns\ServerEntryColumn;
|
||||
use Carbon\CarbonInterface;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Columns\Layout\Stack;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Number;
|
||||
|
||||
class ListServers extends ListRecords
|
||||
{
|
||||
protected static string $resource = ServerResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->paginated(false)
|
||||
->query(fn () => auth()->user()->can('viewList server') ? Server::query() : auth()->user()->accessibleServers())
|
||||
->columns([
|
||||
Stack::make([
|
||||
ServerEntryColumn::make('server_entry')
|
||||
->searchable(['name']),
|
||||
]),
|
||||
])
|
||||
->contentGrid([
|
||||
'default' => 1,
|
||||
'xl' => 2,
|
||||
])
|
||||
->recordUrl(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('You don\'t have access to any servers!');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function uptime(Server $server): string
|
||||
{
|
||||
$uptime = collect(cache()->get("servers.{$server->id}.uptime"))->last() ?? 0;
|
||||
|
||||
if ($uptime === 0) {
|
||||
return 'Offline';
|
||||
}
|
||||
|
||||
return now()->subMillis($uptime)->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE, short: true, parts: 2);
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function cpu(Server $server): string
|
||||
{
|
||||
$cpu = Number::format(collect(cache()->get("servers.{$server->id}.cpu_absolute"))->last() ?? 0, maxPrecision: 2, locale: auth()->user()->language) . '%';
|
||||
$max = Number::format($server->cpu, locale: auth()->user()->language) . '%';
|
||||
|
||||
return $cpu . ($server->cpu > 0 ? ' Of ' . $max : '');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function memory(Server $server): string
|
||||
{
|
||||
$latestMemoryUsed = collect(cache()->get("servers.{$server->id}.memory_bytes"))->last() ?? 0;
|
||||
$totalMemory = collect(cache()->get("servers.{$server->id}.memory_limit_bytes"))->last() ?? 0;
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($latestMemoryUsed / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($latestMemoryUsed / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
if ($totalMemory === 0) {
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($server->memory / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($server->memory / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
} else {
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($totalMemory / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($totalMemory / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
}
|
||||
|
||||
return $used . ($server->memory > 0 ? ' Of ' . $total : '');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
private function disk(Server $server): string
|
||||
{
|
||||
$usedDisk = collect(cache()->get("servers.{$server->id}.disk_bytes"))->last() ?? 0;
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? Number::format($usedDisk / 1024 / 1024 / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($usedDisk / 1000 / 1000 / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? Number::format($server->disk / 1024, maxPrecision: 2, locale: auth()->user()->language) .' GiB'
|
||||
: Number::format($server->disk / 1000, maxPrecision: 2, locale: auth()->user()->language) . ' GB';
|
||||
|
||||
return $used . ($server->disk > 0 ? ' Of ' . $total : '');
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Pages\Auth\Login as BaseLogin;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class Login extends BaseLogin
|
||||
{
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->form(
|
||||
$this->makeForm()
|
||||
->schema([
|
||||
$this->getLoginFormComponent(),
|
||||
$this->getPasswordFormComponent(),
|
||||
$this->getRememberFormComponent(),
|
||||
$this->getOAuthFormComponent(),
|
||||
Turnstile::make('captcha')
|
||||
->hidden(!config('turnstile.turnstile_enabled'))
|
||||
->validationMessages([
|
||||
'required' => config('turnstile.error_messages.turnstile_check_message'),
|
||||
]),
|
||||
])
|
||||
->statePath('data'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected function throwFailureValidationException(): never
|
||||
{
|
||||
$this->dispatch('reset-captcha');
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'data.login' => __('filament-panels::pages/auth/login.messages.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getLoginFormComponent(): Component
|
||||
{
|
||||
return TextInput::make('login')
|
||||
->label('Login')
|
||||
->required()
|
||||
->autocomplete()
|
||||
->autofocus()
|
||||
->extraInputAttributes(['tabindex' => 1]);
|
||||
}
|
||||
|
||||
protected function getOAuthFormComponent(): Component
|
||||
{
|
||||
$actions = [];
|
||||
|
||||
foreach (config('auth.oauth') as $name => $data) {
|
||||
if (!$data['enabled']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$actions[] = Action::make("oauth_$name")
|
||||
->label(Str::title($name))
|
||||
->icon($data['icon'])
|
||||
->color($data['color'])
|
||||
->url(route('auth.oauth.redirect', ['driver' => $name], false));
|
||||
}
|
||||
|
||||
return Actions::make($actions);
|
||||
}
|
||||
|
||||
protected function getCredentialsFromFormData(array $data): array
|
||||
{
|
||||
$loginType = filter_var($data['login'], FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
|
||||
|
||||
return [
|
||||
$loginType => mb_strtolower($data['login']),
|
||||
'password' => $data['password'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource\Pages\CreateNode;
|
||||
use App\Filament\Resources\NodeResource\Pages\ListNodes;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
@@ -40,8 +39,8 @@ class Dashboard extends Page
|
||||
{
|
||||
return [
|
||||
'inDevelopment' => config('app.version') === 'canary',
|
||||
'version' => $this->softwareVersionService->currentPanelVersion(),
|
||||
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
|
||||
'version' => $this->softwareVersionService->versionData()['version'],
|
||||
'latestVersion' => $this->softwareVersionService->getPanel(),
|
||||
'isLatest' => $this->softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
@@ -66,13 +65,13 @@ class Dashboard extends Page
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
|
||||
->icon('tabler-server-2')
|
||||
->url(CreateNode::getUrl()),
|
||||
->url(route('filament.admin.resources.nodes.create')),
|
||||
],
|
||||
'supportActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->url($this->softwareVersionService->getDonations(), true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace App\Filament\Pages\Installer;
|
||||
|
||||
use App\Filament\Pages\Dashboard;
|
||||
use App\Filament\Pages\Installer\Steps\CacheStep;
|
||||
use App\Filament\Pages\Installer\Steps\AdminUserStep;
|
||||
use App\Filament\Pages\Installer\Steps\CompletedStep;
|
||||
use App\Filament\Pages\Installer\Steps\DatabaseStep;
|
||||
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
|
||||
use App\Filament\Pages\Installer\Steps\QueueStep;
|
||||
use App\Filament\Pages\Installer\Steps\RedisStep;
|
||||
use App\Filament\Pages\Installer\Steps\RequirementsStep;
|
||||
use App\Filament\Pages\Installer\Steps\SessionStep;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\CheckMigrationsTrait;
|
||||
@@ -19,10 +19,13 @@ use Filament\Forms\Components\Wizard;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\SimplePage;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
@@ -40,6 +43,8 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
|
||||
protected static string $view = 'filament.pages.installer';
|
||||
|
||||
private User $user;
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
@@ -65,9 +70,10 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
RequirementsStep::make(),
|
||||
EnvironmentStep::make($this),
|
||||
DatabaseStep::make($this),
|
||||
CacheStep::make($this),
|
||||
QueueStep::make($this),
|
||||
SessionStep::make(),
|
||||
RedisStep::make($this)
|
||||
->hidden(fn (Get $get) => $get('env_general.SESSION_DRIVER') != 'redis' && $get('env_general.QUEUE_CONNECTION') != 'redis' && $get('env_general.CACHE_STORE') != 'redis'),
|
||||
AdminUserStep::make($this),
|
||||
CompletedStep::make(),
|
||||
])
|
||||
->persistStepInQueryString()
|
||||
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
|
||||
@@ -89,33 +95,23 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
return 'data';
|
||||
}
|
||||
|
||||
public function submit(UserCreationService $userCreationService): void
|
||||
public function submit(): Redirector|RedirectResponse
|
||||
{
|
||||
try {
|
||||
// Disable installer
|
||||
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
|
||||
// Disable installer
|
||||
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
|
||||
|
||||
// Run migrations
|
||||
$this->runMigrations();
|
||||
// Login user
|
||||
$this->user ??= User::all()->filter(fn ($user) => $user->isRootAdmin())->first();
|
||||
auth()->guard()->login($this->user, true);
|
||||
|
||||
// Create admin user & login
|
||||
$user = $this->createAdminUser($userCreationService);
|
||||
auth()->guard()->login($user, true);
|
||||
|
||||
// Write session data at the very end to avoid "page expired" errors
|
||||
$this->writeToEnv('env_session');
|
||||
|
||||
// Redirect to admin panel
|
||||
$this->redirect(Dashboard::getUrl());
|
||||
} catch (Halt) {
|
||||
}
|
||||
// Redirect to admin panel
|
||||
return redirect(Dashboard::getUrl());
|
||||
}
|
||||
|
||||
public function writeToEnv(string $key): void
|
||||
{
|
||||
try {
|
||||
$variables = array_get($this->data, $key);
|
||||
$variables = array_filter($variables); // Filter array to remove NULL values
|
||||
$this->writeToEnvironment($variables);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
@@ -133,12 +129,13 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
Artisan::call('config:clear');
|
||||
}
|
||||
|
||||
public function runMigrations(): void
|
||||
public function runMigrations(string $driver): void
|
||||
{
|
||||
try {
|
||||
Artisan::call('migrate', [
|
||||
'--force' => true,
|
||||
'--seed' => true,
|
||||
'--database' => $driver,
|
||||
]);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
@@ -164,13 +161,12 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
}
|
||||
}
|
||||
|
||||
public function createAdminUser(UserCreationService $userCreationService): User
|
||||
public function createAdminUser(UserCreationService $userCreationService): void
|
||||
{
|
||||
try {
|
||||
$userData = array_get($this->data, 'user');
|
||||
$userData['root_admin'] = true;
|
||||
|
||||
return $userCreationService->handle($userData);
|
||||
$this->user = $userCreationService->handle($userData);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
|
||||
34
app/Filament/Pages/Installer/Steps/AdminUserStep.php
Normal file
34
app/Filament/Pages/Installer/Steps/AdminUserStep.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
|
||||
class AdminUserStep
|
||||
{
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('user')
|
||||
->label('Admin User')
|
||||
->schema([
|
||||
TextInput::make('user.email')
|
||||
->label('Admin E-Mail')
|
||||
->required()
|
||||
->email()
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Admin Username')
|
||||
->required()
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Admin Password')
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
])
|
||||
->afterValidation(fn (UserCreationService $service) => $installer->createAdminUser($service));
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Redis\RedisManager;
|
||||
|
||||
class CacheStep
|
||||
{
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('cache')
|
||||
->label('Cache')
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_cache.CACHE_STORE')
|
||||
->label('Cache Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::CACHE_DRIVERS)
|
||||
->default(config('cache.default'))
|
||||
->columnSpanFull()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
if ($state !== 'redis') {
|
||||
$set('env_cache.REDIS_HOST', null);
|
||||
$set('env_cache.REDIS_PORT', null);
|
||||
$set('env_cache.REDIS_USERNAME', null);
|
||||
$set('env_cache.REDIS_PASSWORD', null);
|
||||
} else {
|
||||
$set('env_cache.REDIS_HOST', $get('env_cache.REDIS_HOST') ?? '127.0.0.1');
|
||||
$set('env_cache.REDIS_PORT', $get('env_cache.REDIS_PORT') ?? '6379');
|
||||
$set('env_cache.REDIS_USERNAME', null);
|
||||
}
|
||||
}),
|
||||
TextInput::make('env_cache.REDIS_HOST')
|
||||
->label('Redis Host')
|
||||
->placeholder('127.0.0.1')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
|
||||
->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')
|
||||
->placeholder('6379')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your redis server.')
|
||||
->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')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your redis user. Can be empty')
|
||||
->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')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password for your redis user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.password') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
])
|
||||
->afterValidation(function (Get $get, Application $app) use ($installer) {
|
||||
$driver = $get('env_cache.CACHE_STORE');
|
||||
|
||||
if (!self::testConnection($app, $driver, $get('env_cache.REDIS_HOST'), $get('env_cache.REDIS_PORT'), $get('env_cache.REDIS_USERNAME'), $get('env_cache.REDIS_PASSWORD'))) {
|
||||
throw new Halt('Redis connection failed');
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_cache');
|
||||
});
|
||||
}
|
||||
|
||||
private static function testConnection(Application $app, string $driver, ?string $host, null|string|int $port, ?string $username, ?string $password): bool
|
||||
{
|
||||
if ($driver !== 'redis') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$redis = new RedisManager($app, 'predis', [
|
||||
'default' => [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
],
|
||||
]);
|
||||
|
||||
$redis->connection()->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
34
app/Filament/Pages/Installer/Steps/CompletedStep.php
Normal file
34
app/Filament/Pages/Installer/Steps/CompletedStep.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class CompletedStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('complete')
|
||||
->label('Setup complete')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content(new HtmlString('The setup is nearly complete!<br>As last step you need to create a new cronjob that runs every minute to process specific tasks, such as session cleanup and scheduled tasks, and also create a queue worker.')),
|
||||
TextInput::make('crontab')
|
||||
->label(new HtmlString('Run the following command to setup your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
|
||||
->disabled()
|
||||
->hintAction(CopyAction::make())
|
||||
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -'),
|
||||
TextInput::make('queueService')
|
||||
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
|
||||
->disabled()
|
||||
->hintAction(CopyAction::make())
|
||||
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service'),
|
||||
Placeholder::make('')
|
||||
->content('After you finished these two last tasks you can click on "Finish" and use your new panel! Have fun!'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -5,98 +5,70 @@ namespace App\Filament\Pages\Installer\Steps;
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DatabaseStep
|
||||
{
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('database')
|
||||
->label('Database')
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_database.DB_CONNECTION')
|
||||
->label('Database Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::DATABASE_DRIVERS)
|
||||
->default(config('database.default'))
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
$set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel');
|
||||
|
||||
if ($state === 'sqlite') {
|
||||
$set('env_database.DB_HOST', null);
|
||||
$set('env_database.DB_PORT', null);
|
||||
$set('env_database.DB_USERNAME', null);
|
||||
$set('env_database.DB_PASSWORD', null);
|
||||
} else {
|
||||
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
|
||||
$set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306');
|
||||
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
|
||||
}
|
||||
}),
|
||||
TextInput::make('env_database.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->placeholder(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')
|
||||
->label(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->columnSpanFull()
|
||||
->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_general.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
|
||||
->required()
|
||||
->default('database.sqlite'),
|
||||
->default(fn (Get $get) => env('DB_DATABASE', $get('env_general.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
|
||||
TextInput::make('env_database.DB_HOST')
|
||||
->label('Database Host')
|
||||
->placeholder('127.0.0.1')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your database. Make sure it is reachable.')
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
->required(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite')
|
||||
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_HOST', '127.0.0.1') : null)
|
||||
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PORT')
|
||||
->label('Database Port')
|
||||
->placeholder('3306')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your database.')
|
||||
->required(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite')
|
||||
->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'),
|
||||
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_PORT', 3306) : null)
|
||||
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_USERNAME')
|
||||
->label('Database Username')
|
||||
->placeholder('pelican')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your database user.')
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
->required(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite')
|
||||
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_USERNAME', 'pelican') : null)
|
||||
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PASSWORD')
|
||||
->label('Database Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password of your database user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
->default(fn (Get $get) => $get('env_general.DB_CONNECTION') !== 'sqlite' ? env('DB_PASSWORD') : null)
|
||||
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
|
||||
])
|
||||
->afterValidation(function (Get $get) use ($installer) {
|
||||
$driver = $get('env_database.DB_CONNECTION');
|
||||
$driver = $get('env_general.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');
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_database');
|
||||
|
||||
$installer->runMigrations($driver);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,40 @@
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Set;
|
||||
|
||||
class EnvironmentStep
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database',
|
||||
'sync' => 'Sync',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('environment')
|
||||
@@ -26,26 +54,44 @@ class EnvironmentStep
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the URL you access your Panel from.')
|
||||
->required()
|
||||
->default(url('')),
|
||||
Fieldset::make('adminuser')
|
||||
->label('Admin User')
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextInput::make('user.email')
|
||||
->label('E-Mail')
|
||||
->required()
|
||||
->email()
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Username')
|
||||
->required()
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Password')
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
]),
|
||||
->default(url(''))
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('env_general.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://') ? 'true' : 'false')),
|
||||
TextInput::make('env_general.SESSION_SECURE_COOKIE')
|
||||
->hidden()
|
||||
->default(str_starts_with(url(''), 'https://') ? 'true' : 'false'),
|
||||
ToggleButtons::make('env_general.CACHE_STORE')
|
||||
->label('Cache Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::CACHE_DRIVERS)
|
||||
->default(config('cache.default', 'file')),
|
||||
ToggleButtons::make('env_general.SESSION_DRIVER')
|
||||
->label('Session Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::SESSION_DRIVERS)
|
||||
->default(config('session.driver', 'file')),
|
||||
ToggleButtons::make('env_general.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::QUEUE_DRIVERS)
|
||||
->default(config('queue.default', 'database')),
|
||||
ToggleButtons::make('env_general.DB_CONNECTION')
|
||||
->label('Database Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::DATABASE_DRIVERS)
|
||||
->default(config('database.default', 'sqlite')),
|
||||
])
|
||||
->afterValidation(fn () => $installer->writeToEnv('env_general'));
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class QueueStep
|
||||
{
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Sync',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('queue')
|
||||
->label('Queue')
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_queue.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
|
||||
->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.')
|
||||
->accepted(fn () => !file_exists('/.dockerenv'))
|
||||
->inline(false)
|
||||
->validationMessages([
|
||||
'accepted' => 'You need to do both steps before continuing!',
|
||||
])
|
||||
->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!'))
|
||||
->disabled()
|
||||
->hintAction(CopyAction::make())
|
||||
->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.'))
|
||||
->disabled()
|
||||
->hintAction(CopyAction::make())
|
||||
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service')
|
||||
->hidden(fn () => file_exists('/.dockerenv'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->afterValidation(fn () => $installer->writeToEnv('env_queue'));
|
||||
}
|
||||
}
|
||||
82
app/Filament/Pages/Installer/Steps/RedisStep.php
Normal file
82
app/Filament/Pages/Installer/Steps/RedisStep.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class RedisStep
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('redis')
|
||||
->label('Redis')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env_redis.REDIS_HOST')
|
||||
->label('Redis Host')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
|
||||
->required()
|
||||
->default(config('database.redis.default.host')),
|
||||
TextInput::make('env_redis.REDIS_PORT')
|
||||
->label('Redis Port')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your redis server.')
|
||||
->required()
|
||||
->default(config('database.redis.default.port')),
|
||||
TextInput::make('env_redis.REDIS_USERNAME')
|
||||
->label('Redis Username')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your redis user. Can be empty')
|
||||
->default(config('database.redis.default.username')),
|
||||
TextInput::make('env_redis.REDIS_PASSWORD')
|
||||
->label('Redis Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password for your redis user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(config('database.redis.default.password')),
|
||||
])
|
||||
->afterValidation(function (Get $get) use ($installer) {
|
||||
if (!self::testConnection($get('env_redis.REDIS_HOST'), $get('env_redis.REDIS_PORT'), $get('env_redis.REDIS_USERNAME'), $get('env_redis.REDIS_PASSWORD'))) {
|
||||
throw new Halt('Redis connection failed');
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_redis');
|
||||
});
|
||||
}
|
||||
|
||||
private static function testConnection(string $host, null|string|int $port, ?string $username, ?string $password): bool
|
||||
{
|
||||
try {
|
||||
config()->set('database.redis._panel_install_test', [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
Redis::connection('_panel_install_test')->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
|
||||
class SessionStep
|
||||
{
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('session')
|
||||
->label('Session')
|
||||
->schema([
|
||||
ToggleButtons::make('env_session.SESSION_DRIVER')
|
||||
->label('Session Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::SESSION_DRIVERS)
|
||||
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
|
||||
->default(config('session.driver')),
|
||||
TextInput::make('env_session.SESSION_SECURE_COOKIE')
|
||||
->hidden()
|
||||
->default(request()->isSecure()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
@@ -25,11 +24,8 @@ use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
@@ -71,11 +67,10 @@ class Settings extends Page implements HasForms
|
||||
->label('General')
|
||||
->icon('tabler-home')
|
||||
->schema($this->generalSettings()),
|
||||
Tab::make('captcha')
|
||||
->label('Captcha')
|
||||
Tab::make('recaptcha')
|
||||
->label('reCAPTCHA')
|
||||
->icon('tabler-shield')
|
||||
->schema($this->captchaSettings())
|
||||
->columns(3),
|
||||
->schema($this->recaptchaSettings()),
|
||||
Tab::make('mail')
|
||||
->label('Mail')
|
||||
->icon('tabler-mail')
|
||||
@@ -151,7 +146,7 @@ class Settings extends Page implements HasForms
|
||||
->separator()
|
||||
->splitKeys(['Tab', ' '])
|
||||
->placeholder('New IP or IP Range')
|
||||
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
|
||||
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
|
||||
->hintActions([
|
||||
FormAction::make('clear')
|
||||
->label('Clear')
|
||||
@@ -164,71 +159,56 @@ class Settings extends Page implements HasForms
|
||||
->label('Set to Cloudflare IPs')
|
||||
->icon('tabler-brand-cloudflare')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(function (Client $client, Set $set) {
|
||||
$ips = collect();
|
||||
try {
|
||||
$response = $client->request(
|
||||
'GET',
|
||||
'https://api.cloudflare.com/client/v4/ips',
|
||||
config('panel.guzzle')
|
||||
);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$result = json_decode($response->getBody(), true)['result'];
|
||||
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
|
||||
$ips->push(...data_get($result, $value));
|
||||
}
|
||||
$ips->unique();
|
||||
}
|
||||
} catch (GuzzleException $e) {
|
||||
}
|
||||
|
||||
$set('TRUSTED_PROXIES', $ips->values()->all());
|
||||
}),
|
||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
|
||||
'173.245.48.0/20',
|
||||
'103.21.244.0/22',
|
||||
'103.22.200.0/22',
|
||||
'103.31.4.0/22',
|
||||
'141.101.64.0/18',
|
||||
'108.162.192.0/18',
|
||||
'190.93.240.0/20',
|
||||
'188.114.96.0/20',
|
||||
'197.234.240.0/22',
|
||||
'198.41.128.0/17',
|
||||
'162.158.0.0/15',
|
||||
'104.16.0.0/13',
|
||||
'104.24.0.0/14',
|
||||
'172.64.0.0/13',
|
||||
'131.0.72.0/22',
|
||||
])),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function captchaSettings(): array
|
||||
private function recaptchaSettings(): array
|
||||
{
|
||||
return [
|
||||
Toggle::make('TURNSTILE_ENABLED')
|
||||
->label('Enable Turnstile Captcha?')
|
||||
Toggle::make('RECAPTCHA_ENABLED')
|
||||
->label('Enable reCAPTCHA?')
|
||||
->inline(false)
|
||||
->columnSpan(1)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
|
||||
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
|
||||
Placeholder::make('info')
|
||||
->columnSpan(2)
|
||||
->content(new HtmlString('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
|
||||
TextInput::make('TURNSTILE_SITE_KEY')
|
||||
->label('Site Key')
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
|
||||
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
|
||||
TextInput::make('RECAPTCHA_DOMAIN')
|
||||
->label('Domain')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
|
||||
->placeholder('1x00000000000000000000AA'),
|
||||
TextInput::make('TURNSTILE_SECRET_KEY')
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
|
||||
TextInput::make('RECAPTCHA_WEBSITE_KEY')
|
||||
->label('Website Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
|
||||
TextInput::make('RECAPTCHA_SECRET_KEY')
|
||||
->label('Secret Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key')))
|
||||
->placeholder('1x0000000000000000000000000000000AA'),
|
||||
Toggle::make('TURNSTILE_VERIFY_DOMAIN')
|
||||
->label('Verify domain?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state))
|
||||
->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))),
|
||||
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -560,21 +540,7 @@ class Settings extends Page implements HasForms
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
|
||||
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
|
||||
]),
|
||||
Section::make('Webhook')
|
||||
->description('Configure how often old webhook logs should be pruned.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_WEBHOOK_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->default(env('APP_WEBHOOK_PRUNE_DAYS', config('panel.webhook.prune_days'))),
|
||||
]),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CreateApiKey extends CreateRecord
|
||||
{
|
||||
@@ -42,7 +41,7 @@ class CreateApiKey extends CreateRecord
|
||||
'md' => 2,
|
||||
])
|
||||
->schema(
|
||||
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
|
||||
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
|
||||
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||
->options([
|
||||
0 => 'None',
|
||||
@@ -88,20 +87,4 @@ class CreateApiKey extends CreateRecord
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$permissions = [];
|
||||
|
||||
foreach (ApiKey::getPermissionList() as $permission) {
|
||||
if (isset($data['permissions_' . $permission])) {
|
||||
$permissions[$permission] = intval($data['permissions_' . $permission]);
|
||||
unset($data['permissions_' . $permission]);
|
||||
}
|
||||
}
|
||||
|
||||
$data['permissions'] = $permissions;
|
||||
|
||||
return parent::handleRecordCreation($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Filament\Resources\ApiKeyResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ApiKeyResource;
|
||||
use App\Models\ApiKey;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
@@ -36,13 +35,15 @@ class ListApiKeys extends ListRecords
|
||||
->hidden()
|
||||
->searchable(),
|
||||
|
||||
DateTimeColumn::make('last_used_at')
|
||||
TextColumn::make('last_used_at')
|
||||
->label('Last Used')
|
||||
->placeholder('Not Used')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
|
||||
DateTimeColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->label('Created')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('user.username')
|
||||
|
||||
@@ -82,7 +82,6 @@ class CreateDatabaseHost extends CreateRecord
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->unique()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
|
||||
@@ -77,7 +77,6 @@ class EditDatabaseHost extends EditRecord
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
->unique()
|
||||
->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.')
|
||||
->label('Linked Node')
|
||||
->relationship('node', 'name'),
|
||||
@@ -102,13 +101,9 @@ class EditDatabaseHost extends EditRecord
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
if (DatabasesRelationManager::canViewForRecord($this->getRecord(), static::class)) {
|
||||
return [
|
||||
DatabasesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
return [
|
||||
DatabasesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Filament\Resources\DatabaseHostResource\RelationManagers;
|
||||
|
||||
use App\Models\Database;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
@@ -24,30 +23,21 @@ class DatabasesRelationManager extends RelationManager
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('database')
|
||||
->columnSpanFull(),
|
||||
TextInput::make('database')->columnSpanFull(),
|
||||
TextInput::make('username'),
|
||||
TextInput::make('password')
|
||||
->password()
|
||||
->revealable()
|
||||
->hintAction(
|
||||
Action::make('rotate')
|
||||
->icon('tabler-refresh')
|
||||
->requiresConfirmation()
|
||||
->action(fn (DatabasePasswordService $service, Database $database, $set, $get) => $this->rotatePassword($service, $database, $set, $get))
|
||||
->authorize(fn (Database $database) => auth()->user()->can('update database', $database))
|
||||
)
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')
|
||||
->label('Connections From')
|
||||
->formatStateUsing(fn ($record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote),
|
||||
TextInput::make('max_connections')
|
||||
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections),
|
||||
TextInput::make('remote')->label('Connections From'),
|
||||
TextInput::make('max_connections'),
|
||||
TextInput::make('JDBC')
|
||||
->label('JDBC Connection String')
|
||||
->columnSpanFull()
|
||||
->password()
|
||||
->revealable()
|
||||
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
|
||||
]);
|
||||
}
|
||||
@@ -57,25 +47,18 @@ class DatabasesRelationManager extends RelationManager
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->columns([
|
||||
TextColumn::make('database')
|
||||
->icon('tabler-database'),
|
||||
TextColumn::make('username')
|
||||
->icon('tabler-user'),
|
||||
TextColumn::make('remote')
|
||||
->formatStateUsing(fn ($record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote),
|
||||
TextColumn::make('database')->icon('tabler-database'),
|
||||
TextColumn::make('username')->icon('tabler-user'),
|
||||
TextColumn::make('remote'),
|
||||
TextColumn::make('server.name')
|
||||
->icon('tabler-brand-docker')
|
||||
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
|
||||
TextColumn::make('max_connections')
|
||||
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections),
|
||||
DateTimeColumn::make('created_at'),
|
||||
TextColumn::make('max_connections'),
|
||||
TextColumn::make('created_at')->dateTime(),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database)),
|
||||
ViewAction::make()
|
||||
->color('primary')
|
||||
->hidden(fn () => !auth()->user()->can('viewList database')),
|
||||
DeleteAction::make(),
|
||||
ViewAction::make()->color('primary'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseResource;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
@@ -35,10 +34,12 @@ class ListDatabases extends ListRecords
|
||||
TextColumn::make('max_connections')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('created_at')
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
DateTimeColumn::make('updated_at')
|
||||
TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
|
||||
@@ -142,7 +142,6 @@ class ListEggs extends ListRecords
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@@ -159,7 +158,6 @@ class ListEggs extends ListRecords
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use App\Services\Nodes\NodeAutoDeployService;
|
||||
use App\Services\Nodes\NodeUpdateService;
|
||||
use Filament\Actions;
|
||||
@@ -56,7 +55,7 @@ class EditNode extends EditRecord
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->label('Wings Version')
|
||||
->content(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? 'Unknown') . ' (Latest: ' . $versionService->latestWingsVersion() . ')'),
|
||||
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
|
||||
Placeholder::make('')
|
||||
->label('CPU Threads')
|
||||
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
|
||||
|
||||
@@ -48,7 +48,6 @@ class AllocationsRelationManager extends RelationManager
|
||||
// All assigned allocations
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||
->searchable()
|
||||
->selectCurrentPageOnly() //Prevent people from trying to nuke 30,000 ports at once.... -,-
|
||||
->columns([
|
||||
TextColumn::make('id'),
|
||||
TextColumn::make('port')
|
||||
@@ -66,6 +65,12 @@ class AllocationsRelationManager extends RelationManager
|
||||
->searchable()
|
||||
->label('IP'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
|
||||
->form(fn () => [
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enums\RolePermissionModels;
|
||||
use App\Enums\RolePermissionPrefixes;
|
||||
use App\Filament\Resources\RoleResource\Pages;
|
||||
use App\Models\Role;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Component;
|
||||
@@ -70,7 +71,7 @@ class RoleResource extends Resource
|
||||
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
TextInput::make('guard_name')
|
||||
->label('Guard Name')
|
||||
->default(Role::DEFAULT_GUARD_NAME)
|
||||
->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '')
|
||||
->nullable()
|
||||
->hidden(),
|
||||
Fieldset::make('Permissions')
|
||||
@@ -91,8 +92,6 @@ class RoleResource extends Resource
|
||||
$icon = ('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon();
|
||||
} elseif (class_exists('\App\Filament\Pages\\' . $model)) {
|
||||
$icon = ('\App\Filament\Pages\\' . $model)::getNavigationIcon();
|
||||
} elseif (class_exists('\App\Filament\Server\Resources\\' . $model . 'Resource')) {
|
||||
$icon = ('\App\Filament\Server\Resources\\' . $model . 'Resource')::getNavigationIcon();
|
||||
}
|
||||
|
||||
return Section::make(Str::headline(Str::plural($model)))
|
||||
|
||||
@@ -113,7 +113,6 @@ class CreateServer extends CreateRecord
|
||||
TextInput::make('username')
|
||||
->alphaNum()
|
||||
->required()
|
||||
->minLength(3)
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('email')
|
||||
@@ -788,7 +787,6 @@ class CreateServer extends CreateRecord
|
||||
->schema([
|
||||
Select::make('select_image')
|
||||
->label('Image Name')
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
|
||||
->options(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
@@ -813,7 +811,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
TextInput::make('image')
|
||||
->label('Image')
|
||||
->required()
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
|
||||
@@ -5,20 +5,16 @@ namespace App\Filament\Resources\ServerResource\Pages;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Filament\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
|
||||
use App\Http\Controllers\Admin\ServersController;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use App\Services\Servers\ServerDeletionService;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\ToggleInstallService;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
@@ -29,7 +25,6 @@ use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
@@ -422,7 +417,6 @@ class EditServer extends EditRecord
|
||||
->schema([
|
||||
Select::make('select_image')
|
||||
->label('Image Name')
|
||||
->live()
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('image', $state))
|
||||
->options(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
@@ -442,7 +436,7 @@ class EditServer extends EditRecord
|
||||
|
||||
TextInput::make('image')
|
||||
->label('Image')
|
||||
->required()
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Get $get, Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
$images = $egg->docker_images ?? [];
|
||||
@@ -456,7 +450,7 @@ class EditServer extends EditRecord
|
||||
->placeholder('Enter a custom Image')
|
||||
->columnSpan(2),
|
||||
|
||||
KeyValue::make('docker_labels')
|
||||
Forms\Components\KeyValue::make('docker_labels')
|
||||
->label('Container Labels')
|
||||
->keyLabel('Label Name')
|
||||
->valueLabel('Label Description')
|
||||
@@ -512,19 +506,23 @@ class EditServer extends EditRecord
|
||||
->label('Startup Command')
|
||||
->required()
|
||||
->columnSpan(6)
|
||||
->autosize(),
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
0
|
||||
);
|
||||
}),
|
||||
|
||||
Textarea::make('defaultStartup')
|
||||
->hintAction(CopyAction::make())
|
||||
->label('Default Startup Command')
|
||||
->disabled()
|
||||
->autosize()
|
||||
->columnSpan(6)
|
||||
->formatStateUsing(function ($state, Get $get) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
|
||||
return $egg->startup;
|
||||
}),
|
||||
})
|
||||
->columnSpan(6),
|
||||
|
||||
Repeater::make('server_variables')
|
||||
->relationship('serverVariables', function (Builder $query) {
|
||||
@@ -598,16 +596,14 @@ class EditServer extends EditRecord
|
||||
->schema([
|
||||
CheckboxList::make('mounts')
|
||||
->relationship('mounts')
|
||||
->options(fn (Server $server) => $server->node->mounts->filter(fn (Mount $mount) => $mount->eggs->contains($server->egg))->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
|
||||
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn (Mount $mount) => [$mount->id => "$mount->source -> $mount->target"]))
|
||||
->options(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => $mount->name]))
|
||||
->descriptions(fn (Server $server) => $server->node->mounts->mapWithKeys(fn ($mount) => [$mount->id => "$mount->source -> $mount->target"]))
|
||||
->label('Mounts')
|
||||
->helperText(fn (Server $server) => $server->node->mounts->isNotEmpty() ? '' : 'No Mounts exist for this Node')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Tab::make('Databases')
|
||||
->hidden(fn () => !auth()->user()->can('viewList database'))
|
||||
->icon('tabler-database')
|
||||
->columns(4)
|
||||
->schema([
|
||||
Repeater::make('databases')
|
||||
->grid()
|
||||
@@ -621,50 +617,35 @@ class EditServer extends EditRecord
|
||||
->formatStateUsing(fn ($record) => $record->database)
|
||||
->hintAction(
|
||||
Action::make('Delete')
|
||||
->authorize(fn (Database $database) => auth()->user()->can('delete database', $database))
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->modalIcon('tabler-database-x')
|
||||
->modalHeading('Delete Database?')
|
||||
->modalSubmitActionLabel(fn (Get $get) => 'Delete ' . $get('database') . '?')
|
||||
->modalDescription(fn (Get $get) => 'Are you sure you want to delete ' . $get('database') . '?')
|
||||
->action(function (DatabaseManagementService $databaseManagementService, $record) {
|
||||
$databaseManagementService->delete($record);
|
||||
$this->fillForm();
|
||||
})
|
||||
->action(fn (DatabaseManagementService $databaseManagementService, $record) => $databaseManagementService->delete($record))
|
||||
),
|
||||
TextInput::make('username')
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($record) => $record->username)
|
||||
->columnSpan(1),
|
||||
->columnSpan(2),
|
||||
TextInput::make('password')
|
||||
->disabled()
|
||||
->password()
|
||||
->revealable()
|
||||
->columnSpan(1)
|
||||
->hintAction(
|
||||
Action::make('rotate')
|
||||
->authorize(fn (Database $database) => auth()->user()->can('update database', $database))
|
||||
->icon('tabler-refresh')
|
||||
->modalHeading('Change Database Password?')
|
||||
->action(fn (DatabasePasswordService $service, $record, $set, $get) => $this->rotatePassword($service, $record, $set, $get))
|
||||
->requiresConfirmation()
|
||||
->action(fn (DatabasePasswordService $service, $record, $set, $get) => $this->rotatePassword($service, $record, $set, $get))
|
||||
)
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
->formatStateUsing(fn (Database $database) => $database->password)
|
||||
->columnSpan(2),
|
||||
TextInput::make('remote')
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($record) => $record->remote === '%' ? 'Anywhere ( % )' : $record->remote)
|
||||
->formatStateUsing(fn ($record) => $record->remote)
|
||||
->columnSpan(1)
|
||||
->label('Connections From'),
|
||||
TextInput::make('max_connections')
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($record) => $record->max_connections === 0 ? 'Unlimited' : $record->max_connections)
|
||||
->formatStateUsing(fn ($record) => $record->max_connections)
|
||||
->columnSpan(1),
|
||||
TextInput::make('JDBC')
|
||||
->disabled()
|
||||
->password()
|
||||
->revealable()
|
||||
->label('JDBC Connection String')
|
||||
->columnSpan(2)
|
||||
->formatStateUsing(fn (Get $get, $record) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($record->password) . '@' . $record->host->host . ':' . $record->host->port . '/' . $get('database')),
|
||||
@@ -673,57 +654,7 @@ class EditServer extends EditRecord
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->columnSpan(4),
|
||||
Forms\Components\Actions::make([
|
||||
Action::make('createDatabase')
|
||||
->authorize(fn () => auth()->user()->can('create database'))
|
||||
->disabled(fn () => DatabaseHost::query()->count() < 1)
|
||||
->label(fn () => DatabaseHost::query()->count() < 1 ? 'No Database Hosts' : 'Create Database')
|
||||
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
|
||||
->modalSubmitActionLabel('Create Database')
|
||||
->action(function (array $data, DatabaseManagementService $service, Server $server, RandomWordService $randomWordService) {
|
||||
if (empty($data['database'])) {
|
||||
$data['database'] = $randomWordService->word() . random_int(1, 420);
|
||||
}
|
||||
if (empty($data['remote'])) {
|
||||
$data['remote'] = '%';
|
||||
}
|
||||
|
||||
$data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id);
|
||||
|
||||
try {
|
||||
$service->setValidateDatabaseLimit(false)->create($server, $data);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title('Failed to Create Database')
|
||||
->body($e->getMessage())
|
||||
->danger()
|
||||
->persistent()->send();
|
||||
}
|
||||
$this->fillForm();
|
||||
})
|
||||
->form([
|
||||
Select::make('database_host_id')
|
||||
->label('Database Host')
|
||||
->required()
|
||||
->placeholder('Select Database Host')
|
||||
->relationship('node.databaseHosts', 'name')
|
||||
->default(fn () => (DatabaseHost::query()->first())?->id)
|
||||
->selectablePlaceholder(false),
|
||||
TextInput::make('database')
|
||||
->label('Database Name')
|
||||
->alphaDash()
|
||||
->prefix(fn (Server $server) => 's' . $server->id . '_')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Leaving this blank will auto generate a random name'),
|
||||
TextInput::make('remote')
|
||||
->columnSpan(1)
|
||||
->regex('/^[\w\-\/.%:]+$/')
|
||||
->label('Connections From')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Where connections should be allowed from. Leave blank to allow connections from anywhere.'),
|
||||
]),
|
||||
])->alignCenter()->columnSpanFull(),
|
||||
]),
|
||||
])->columns(4),
|
||||
Tab::make('Actions')
|
||||
->icon('tabler-settings')
|
||||
->schema([
|
||||
@@ -742,8 +673,8 @@ class EditServer extends EditRecord
|
||||
Action::make('toggleInstall')
|
||||
->label('Toggle Install Status')
|
||||
->disabled(fn (Server $server) => $server->isSuspended())
|
||||
->action(function (ToggleInstallService $service, Server $server) {
|
||||
$service->handle($server);
|
||||
->action(function (ServersController $serversController, Server $server) {
|
||||
$serversController->toggleInstall($server);
|
||||
|
||||
$this->refreshFormData(['status', 'docker']);
|
||||
}),
|
||||
@@ -829,7 +760,7 @@ class EditServer extends EditRecord
|
||||
->modalHeading('Are you sure you want to reinstall this server?')
|
||||
->modalDescription('!! This can result in unrecoverable data loss !!')
|
||||
->disabled(fn (Server $server) => $server->isSuspended())
|
||||
->action(fn (ReinstallServerService $service, Server $server) => $service->handle($server)),
|
||||
->action(fn (ServersController $serversController, Server $server) => $serversController->reinstallServer($server)),
|
||||
])->fullWidth(),
|
||||
ToggleButtons::make('')
|
||||
->hint('This will reinstall the server with the assigned egg install script.'),
|
||||
@@ -864,7 +795,7 @@ class EditServer extends EditRecord
|
||||
->action(function (Server $server, ServerDeletionService $service) {
|
||||
$service->handle($server);
|
||||
|
||||
return redirect(ListServers::getUrl(panel: 'admin'));
|
||||
return redirect(ListServers::getUrl());
|
||||
})
|
||||
->authorize(fn (Server $server) => auth()->user()->can('delete server', $server)),
|
||||
Actions\Action::make('console')
|
||||
@@ -895,7 +826,7 @@ class EditServer extends EditRecord
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
AllocationsRelationManager::class,
|
||||
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Models\Server;
|
||||
use Filament\Actions;
|
||||
@@ -83,8 +82,8 @@ class ListServers extends ListRecords
|
||||
->actions([
|
||||
Action::make('View')
|
||||
->icon('tabler-terminal')
|
||||
->url(fn (Server $server) => Console::getUrl(panel: 'server', tenant: $server))
|
||||
->authorize(fn (Server $server) => auth()->user()->canAccessTenant($server)),
|
||||
->url(fn (Server $server) => "/server/$server->uuid_short")
|
||||
->authorize(fn () => auth()->user()->can('view server')),
|
||||
EditAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
|
||||
@@ -40,7 +40,6 @@ class AllocationsRelationManager extends RelationManager
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->selectCurrentPageOnly()
|
||||
->recordTitleAttribute('ip')
|
||||
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
|
||||
@@ -150,7 +149,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
->multiple()
|
||||
->associateAnother(false)
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
|
||||
->label('Add Allocation'),
|
||||
])
|
||||
->bulkActions([
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Models\ApiKey;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\ToggleTwoFactorService;
|
||||
use App\Services\Users\TwoFactorSetupService;
|
||||
use App\Services\Users\UserUpdateService;
|
||||
use chillerlan\QRCode\Common\EccLevel;
|
||||
use chillerlan\QRCode\Common\Version;
|
||||
use chillerlan\QRCode\QRCode;
|
||||
@@ -17,7 +16,6 @@ use chillerlan\QRCode\QROptions;
|
||||
use Closure;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
@@ -31,14 +29,11 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
|
||||
/**
|
||||
* @method User getUser()
|
||||
@@ -52,11 +47,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
$this->toggleTwoFactorService = $toggleTwoFactorService;
|
||||
}
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
}
|
||||
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
@@ -123,53 +113,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->options(fn (User $user) => $user->getAvailableLanguages()),
|
||||
]),
|
||||
|
||||
Tab::make('OAuth')
|
||||
->icon('tabler-brand-oauth')
|
||||
->visible(function () {
|
||||
foreach (config('auth.oauth') as $name => $data) {
|
||||
if ($data['enabled']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
->schema(function () {
|
||||
$providers = [];
|
||||
|
||||
foreach (config('auth.oauth') as $name => $data) {
|
||||
if (!$data['enabled']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$unlink = array_key_exists($name, $this->getUser()->oauth);
|
||||
|
||||
$providers[] = Action::make("oauth_$name")
|
||||
->label(($unlink ? 'Unlink ' : 'Link ') . Str::title($name))
|
||||
->icon($unlink ? 'tabler-unlink' : 'tabler-link')
|
||||
->color($data['color'])
|
||||
->action(function (UserUpdateService $updateService) use ($name, $unlink) {
|
||||
if ($unlink) {
|
||||
$oauth = auth()->user()->oauth;
|
||||
unset($oauth[$name]);
|
||||
|
||||
$updateService->handle(auth()->user(), ['oauth' => $oauth]);
|
||||
|
||||
$this->fillForm();
|
||||
|
||||
Notification::make()
|
||||
->title("OAuth provider '$name' unlinked")
|
||||
->success()
|
||||
->send();
|
||||
} elseif (config("auth.oauth.$name.enabled")) {
|
||||
redirect(Socialite::with($name)->redirect()->getTargetUrl());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return [Actions::make($providers)];
|
||||
}),
|
||||
|
||||
Tab::make('2FA')
|
||||
->icon('tabler-shield-lock')
|
||||
->schema(function (TwoFactorSetupService $setupService) {
|
||||
@@ -242,7 +185,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->content(fn () => new HtmlString("
|
||||
<div style='width: 300px; background-color: rgb(24, 24, 27);'>$image</div>
|
||||
"))
|
||||
->helperText('Setup Key: ' . $secret),
|
||||
->helperText('Setup Key: '. $secret),
|
||||
TextInput::make('2facode')
|
||||
->label('Code')
|
||||
->requiredWith('2fapassword')
|
||||
@@ -346,7 +289,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
|
||||
if ($token = $data['2facode'] ?? null) {
|
||||
$tokens = $this->toggleTwoFactorService->handle($record, $token, true);
|
||||
cache()->put("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
|
||||
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
|
||||
|
||||
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
|
||||
}
|
||||
@@ -374,17 +317,4 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
$stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class EditUser extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
TextInput::make('username')->required()->minLength(3)->maxLength(255),
|
||||
TextInput::make('username')->required()->maxLength(255),
|
||||
TextInput::make('email')->email()->required()->maxLength(255),
|
||||
TextInput::make('password')
|
||||
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||
|
||||
@@ -91,8 +91,6 @@ class ListUsers extends ListRecords
|
||||
TextInput::make('username')
|
||||
->alphaNum()
|
||||
->required()
|
||||
->unique()
|
||||
->minLength(3)
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
->email()
|
||||
|
||||
@@ -4,7 +4,13 @@ namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\WebhookResource\Pages;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class WebhookResource extends Resource
|
||||
{
|
||||
@@ -16,9 +22,41 @@ class WebhookResource extends Resource
|
||||
|
||||
protected static ?string $label = 'Webhooks';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('endpoint')->activeUrl()->required(),
|
||||
TextInput::make('description')->nullable(),
|
||||
CheckboxList::make('events')->lazy()->options(
|
||||
fn () => WebhookConfiguration::filamentCheckboxList()
|
||||
)
|
||||
->searchable()
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
->columnSpanFull()
|
||||
->gridDirection('row')
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('description'),
|
||||
TextColumn::make('endpoint'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
|
||||
@@ -3,34 +3,9 @@
|
||||
namespace App\Filament\Resources\WebhookResource\Pages;
|
||||
|
||||
use App\Filament\Resources\WebhookResource;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateWebhookConfiguration extends CreateRecord
|
||||
{
|
||||
protected static string $resource = WebhookResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('endpoint')
|
||||
->activeUrl()
|
||||
->required(),
|
||||
TextInput::make('description')
|
||||
->required(),
|
||||
CheckboxList::make('events')
|
||||
->lazy()
|
||||
->options(fn () => WebhookConfiguration::filamentCheckboxList())
|
||||
->searchable()
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
->columnSpanFull()
|
||||
->gridDirection('row')
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,56 +2,18 @@
|
||||
|
||||
namespace App\Filament\Resources\WebhookResource\Pages;
|
||||
|
||||
use App\Models\WebhookConfiguration;
|
||||
use App\Filament\Resources\WebhookResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditWebhookConfiguration extends EditRecord
|
||||
{
|
||||
protected static string $resource = WebhookResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('endpoint')
|
||||
->label('Endpoint')
|
||||
->activeUrl()
|
||||
->required(),
|
||||
TextInput::make('description')
|
||||
->label('Description')
|
||||
->required(),
|
||||
CheckboxList::make('events')
|
||||
->label('Events')
|
||||
->lazy()
|
||||
->options(fn () => WebhookConfiguration::filamentCheckboxList())
|
||||
->searchable()
|
||||
->bulkToggleable()
|
||||
->columns(3)
|
||||
->columnSpanFull()
|
||||
->gridDirection('row')
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make()
|
||||
->label('Delete')
|
||||
->modalHeading('Are you sure you want to delete this?')
|
||||
->modalDescription('')
|
||||
->modalSubmitActionLabel('Delete'),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,50 +3,17 @@
|
||||
namespace App\Filament\Resources\WebhookResource\Pages;
|
||||
|
||||
use App\Filament\Resources\WebhookResource;
|
||||
use App\Models\WebhookConfiguration;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
|
||||
class ListWebhookConfigurations extends ListRecords
|
||||
{
|
||||
protected static string $resource = WebhookResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('description')
|
||||
->label('Description'),
|
||||
TextColumn::make('endpoint')
|
||||
->label('Endpoint'),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->label('Delete'),
|
||||
EditAction::make()
|
||||
->label('Edit'),
|
||||
])
|
||||
->emptyStateIcon('tabler-webhook')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Webhooks')
|
||||
->emptyStateActions([
|
||||
CreateAction::make('create')
|
||||
->label('Create Webhook')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->label('Create Webhook')
|
||||
->hidden(fn () => WebhookConfiguration::count() <= 0),
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Filament\Server\Widgets\ServerConsole;
|
||||
use App\Filament\Server\Widgets\ServerCpuChart;
|
||||
use App\Filament\Server\Widgets\ServerMemoryChart;
|
||||
// use App\Filament\Server\Widgets\ServerNetworkChart;
|
||||
use App\Filament\Server\Widgets\ServerOverview;
|
||||
use App\Models\Server;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
class Console extends Page
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-brand-tabler';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static string $view = 'filament.server.pages.console';
|
||||
|
||||
public function getWidgetData(): array
|
||||
{
|
||||
return [
|
||||
'server' => Filament::getTenant(),
|
||||
'user' => auth()->user(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
ServerOverview::class,
|
||||
ServerConsole::class,
|
||||
ServerCpuChart::class,
|
||||
ServerMemoryChart::class,
|
||||
//ServerNetworkChart::class, TODO: convert units.
|
||||
];
|
||||
}
|
||||
|
||||
public function getVisibleWidgets(): array
|
||||
{
|
||||
return $this->filterVisibleWidgets($this->getWidgets());
|
||||
}
|
||||
|
||||
public function getColumns(): int|string|array
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Action::make('start')
|
||||
->color('primary')
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'start'))
|
||||
->disabled(fn () => $server->isInConflictState()),
|
||||
Action::make('restart')
|
||||
->color('gray')
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'restart'))
|
||||
->disabled(fn () => $server->isInConflictState() || $server->retrieveStatus() == 'offline'),
|
||||
Action::make('stop')
|
||||
->color('danger')
|
||||
->action(fn () => $this->dispatch('setServerState', state: 'stop'))
|
||||
->disabled(fn () => $server->isInConflictState() || $server->retrieveStatus() == 'offline'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Pages\Concerns\InteractsWithFormActions;
|
||||
use Filament\Pages\Page;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
abstract class ServerFormPage extends Page
|
||||
{
|
||||
use InteractsWithFormActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $view = 'filament.server.pages.server-form-page';
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void {}
|
||||
|
||||
protected function fillForm(): void
|
||||
{
|
||||
$data = $this->getRecord()->attributesToArray();
|
||||
$this->form->fill($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int | string, string | Form>
|
||||
*/
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->form($this->makeForm()
|
||||
->model($this->getRecord())
|
||||
->statePath($this->getFormStatePath())
|
||||
->columns($this->hasInlineLabels() ? 1 : 2)
|
||||
->inlineLabel($this->hasInlineLabels()),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
public function getRecord(): Server
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Settings extends ServerFormPage
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Section::make('Server Information')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Fieldset::make('Server')
|
||||
->label('Information')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Server Name')
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
|
||||
->required()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateName($state, $server)),
|
||||
Textarea::make('description')
|
||||
->label('Server Description')
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->autosize()
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
|
||||
TextInput::make('uuid')
|
||||
->label('Server UUID')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 5,
|
||||
])
|
||||
->disabled(),
|
||||
TextInput::make('id')
|
||||
->label('Server ID')
|
||||
->disabled()
|
||||
->columnSpan(1),
|
||||
]),
|
||||
Fieldset::make('Limits')
|
||||
->label('Limits')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('backup_limit')
|
||||
->label('Backup Limit')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Backups can be created' : $server->backups->count() . ' of ' . $state),
|
||||
TextInput::make('database_limit')
|
||||
->label('Database Limit')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No Databases can be created' : $server->databases->count() . ' of ' . $state),
|
||||
TextInput::make('allocation_limit')
|
||||
->label('Allocation Limit')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn ($state, Server $server) => !$state ? 'No additional Allocations can be created' : $server->allocations->count() . ' of ' . ($state + 1)),
|
||||
]),
|
||||
]),
|
||||
Section::make('Node Information')
|
||||
->schema([
|
||||
TextInput::make('node.name')
|
||||
->label('Node Name')
|
||||
->formatStateUsing(fn (Server $server) => $server->node->name)
|
||||
->disabled(),
|
||||
Fieldset::make('SFTP Information')
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_FILE_SFTP, $server))
|
||||
->label('SFTP Information')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('connection')
|
||||
->label('Connection')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->hintActions([
|
||||
Action::make('connect_sftp')
|
||||
->label('Connect to SFTP')
|
||||
->color('success')
|
||||
->icon('tabler-plug')
|
||||
->url(function (Server $server) {
|
||||
$fqdn = $server->node->daemon_sftp_alias ?? $server->node->fqdn;
|
||||
|
||||
return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
}),
|
||||
])
|
||||
->formatStateUsing(function (Server $server) {
|
||||
$fqdn = $server->node->daemon_sftp_alias ?? $server->node->fqdn;
|
||||
|
||||
return 'sftp://' . auth()->user()->username . '.' . $server->uuid_short . '@' . $fqdn . ':' . $server->node->daemon_sftp;
|
||||
}),
|
||||
TextInput::make('username')
|
||||
->label('Username')
|
||||
->columnSpan(1)
|
||||
->disabled()
|
||||
->formatStateUsing(fn (Server $server) => auth()->user()->username . '.' . $server->uuid_short),
|
||||
Placeholder::make('password')
|
||||
->columnSpan(1)
|
||||
->content('Your SFTP password is the same as the password you use to access this panel.'),
|
||||
]),
|
||||
]),
|
||||
Section::make('Reinstall Server')
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server))
|
||||
->collapsible()->collapsed()
|
||||
->footerActions([
|
||||
Action::make('reinstall')
|
||||
->color('danger')
|
||||
->disabled(!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')
|
||||
->action(function (Server $server) {
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_REINSTALL, $server), 403);
|
||||
|
||||
$server->fill(['status' => ServerState::Installing])->save();
|
||||
try {
|
||||
Http::daemon($server->node)->post(sprintf(
|
||||
'/api/servers/%s/reinstall',
|
||||
$server->uuid
|
||||
));
|
||||
} catch (TransferException $exception) {
|
||||
throw new DaemonConnectionException($exception);
|
||||
}
|
||||
|
||||
Activity::event('server:settings.reinstall')
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Server Reinstall Started')
|
||||
->send();
|
||||
}),
|
||||
])
|
||||
->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.'),
|
||||
Placeholder::make('')
|
||||
->label('Some files may be deleted or modified during this process, please back up your data before continuing.'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateName(string $name, Server $server): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server), 403);
|
||||
|
||||
$original = $server->name;
|
||||
|
||||
try {
|
||||
$server->forceFill([
|
||||
'name' => $name,
|
||||
])->saveOrFail();
|
||||
|
||||
if ($original !== $name) {
|
||||
Activity::event('server:settings.rename')
|
||||
->property(['old' => $original, 'new' => $name])
|
||||
->log();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Updated Server Name')
|
||||
->body(fn () => $original . ' -> ' . $name)
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed')
|
||||
->body($exception->getMessage())
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function updateDescription(string $description, Server $server): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_SETTINGS_RENAME, $server), 403);
|
||||
|
||||
$original = $server->description;
|
||||
|
||||
try {
|
||||
$server->forceFill([
|
||||
'description' => $description,
|
||||
])->saveOrFail();
|
||||
|
||||
if ($original !== $description) {
|
||||
Activity::event('server:settings.description')
|
||||
->property(['old' => $original, 'new' => $description])
|
||||
->log();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Updated Server Description')
|
||||
->body(fn () => $original . ' -> ' . $description)
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed')
|
||||
->body($exception->getMessage())
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use Closure;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class Startup extends ServerFormPage
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-player-play';
|
||||
|
||||
protected static ?int $navigationSort = 9;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Textarea::make('startup')
|
||||
->label('Startup Command')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->autosize()
|
||||
->readOnly(),
|
||||
TextInput::make('custom_image')
|
||||
->label('Docker Image')
|
||||
->readOnly()
|
||||
->visible(fn (Server $server) => !in_array($server->image, $server->egg->docker_images))
|
||||
->formatStateUsing(fn (Server $server) => $server->image)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Select::make('image')
|
||||
->label('Docker Image')
|
||||
->live()
|
||||
->visible(fn (Server $server) => in_array($server->image, $server->egg->docker_images))
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
|
||||
->afterStateUpdated(function ($state, Server $server) {
|
||||
$original = $server->image;
|
||||
$server->forceFill(['image' => $state])->saveOrFail();
|
||||
|
||||
if ($original !== $server->image) {
|
||||
Activity::event('server:startup.image')
|
||||
->property(['old' => $original, 'new' => $state])
|
||||
->log();
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Docker image updated')
|
||||
->body('Restart the server to use the new image.')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->options(function (Server $server) {
|
||||
$images = $server->egg->docker_images;
|
||||
|
||||
return array_flip($images);
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
]),
|
||||
Section::make('Server Variables')
|
||||
->schema([
|
||||
Repeater::make('server_variables')
|
||||
->label('')
|
||||
->relationship('viewableServerVariables')
|
||||
->grid()
|
||||
->disabled(!auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
|
||||
->reorderable(false)->addable(false)->deletable(false)
|
||||
->schema(function () {
|
||||
$text = TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->disabled(fn (ServerVariable $serverVariable) => !$serverVariable->variable->user_editable)
|
||||
->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(...))
|
||||
->disabled(fn (ServerVariable $serverVariable) => !$serverVariable->variable->user_editable)
|
||||
->options($this->getSelectOptionsFromRules(...))
|
||||
->selectablePlaceholder(false);
|
||||
|
||||
$components = [$text, $select];
|
||||
|
||||
foreach ($components as &$component) {
|
||||
$component = $component
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function ($state, ServerVariable $serverVariable) {
|
||||
$this->update($state, $serverVariable);
|
||||
})
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
|
||||
return $components;
|
||||
})
|
||||
->columnSpan(6),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_STARTUP_READ, Filament::getTenant()), 403);
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_STARTUP_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
private function shouldHideComponent(ServerVariable $serverVariable, 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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public function update(?string $state, ServerVariable $serverVariable): null
|
||||
{
|
||||
$original = $serverVariable->variable_value;
|
||||
|
||||
try {
|
||||
|
||||
$validator = Validator::make(
|
||||
['variable_value' => $state],
|
||||
['variable_value' => $serverVariable->variable->rules]
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Validation Failed: ' . $serverVariable->variable->name)
|
||||
->body(implode(', ', $validator->errors()->all()))
|
||||
->send();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ServerVariable::query()->updateOrCreate([
|
||||
'server_id' => $this->getRecord()->id,
|
||||
'variable_id' => $serverVariable->variable->id,
|
||||
], [
|
||||
'variable_value' => $state ?? '',
|
||||
]);
|
||||
|
||||
if ($original !== $state) {
|
||||
Activity::event('server:startup.edit')
|
||||
->property([
|
||||
'variable' => $serverVariable->variable->env_variable,
|
||||
'old' => $original,
|
||||
'new' => $state,
|
||||
])
|
||||
->log();
|
||||
}
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Updated: ' . $serverVariable->variable->name)
|
||||
->body(fn () => $original . ' -> ' . $state)
|
||||
->send();
|
||||
} catch (\Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Failed: ' . $serverVariable->variable->name)
|
||||
->body($e->getMessage())
|
||||
->send();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\ActivityResource\Pages;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class ActivityResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ActivityLog::class;
|
||||
|
||||
protected static ?string $label = 'Activity';
|
||||
|
||||
protected static ?string $pluralLabel = 'Activity';
|
||||
|
||||
protected static ?int $navigationSort = 8;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-stack';
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $server->activity()
|
||||
->getQuery()
|
||||
->whereNotIn('activity_logs.event', ActivityLog::DISABLED_EVENTS)
|
||||
->when(config('activity.hide_admin_activity'), function (Builder $builder) use ($server) {
|
||||
// We could do this with a query and a lot of joins, but that gets pretty
|
||||
// painful so for now we'll execute a simpler query.
|
||||
$subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]);
|
||||
$rootAdmins = Role::getRootAdmin()->users()->pluck('id');
|
||||
|
||||
$builder->select('activity_logs.*')
|
||||
->leftJoin('users', function (JoinClause $join) {
|
||||
$join->on('users.id', 'activity_logs.actor_id')
|
||||
->where('activity_logs.actor_type', (new User())->getMorphClass());
|
||||
})
|
||||
->where(function (Builder $builder) use ($subusers, $rootAdmins) {
|
||||
$builder->whereNull('users.id')
|
||||
->orWhereNotIn('users.id', $rootAdmins)
|
||||
->orWhereIn('users.id', $subusers);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ACTIVITY_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListActivities::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ActivityResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\ActivityResource;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\User;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListActivities extends ListRecords
|
||||
{
|
||||
protected static string $resource = ActivityResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('event')
|
||||
->html()
|
||||
->formatStateUsing(fn ($state, ActivityLog $activityLog) => __('activity.'.str($state)->replace(':', '.'))) // TODO: convert properties to a format that trans likes, see ActivityLogEntry.tsx - wrapProperties
|
||||
->description(fn ($state) => $state),
|
||||
TextColumn::make('user')
|
||||
->state(fn (ActivityLog $activityLog) => $activityLog->actor instanceof User ? $activityLog->actor->username : 'System')
|
||||
->tooltip(fn (ActivityLog $activityLog) => auth()->user()->can('seeIps activityLog') ? $activityLog->ip : '')
|
||||
->url(fn (ActivityLog $activityLog): string => $activityLog->actor instanceof User ? route('filament.admin.resources.users.edit', ['record' => $activityLog->actor]) : ''),
|
||||
DateTimeColumn::make('timestamp')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('timestamp', 'desc');
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\AllocationResource\Pages;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AllocationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Allocation::class;
|
||||
|
||||
protected static ?string $label = 'Network';
|
||||
|
||||
protected static ?string $pluralLabel = 'Network';
|
||||
|
||||
protected static ?int $navigationSort = 7;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-network';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ALLOCATION_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListAllocations::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\AllocationResource\Pages;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\AllocationResource;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\FindAssignableAllocationService;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DetachAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListAllocations extends ListRecords
|
||||
{
|
||||
protected static string $resource = AllocationResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('ip')
|
||||
->label('Address')
|
||||
->formatStateUsing(fn (Allocation $allocation) => $allocation->alias),
|
||||
TextColumn::make('alias')
|
||||
->hidden(),
|
||||
TextColumn::make('port'),
|
||||
TextInputColumn::make('notes')
|
||||
->disabled(fn () => !auth()->user()->can(Permission::ACTION_ALLOCATION_UPDATE, $server))
|
||||
->label('Notes'),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(function (Allocation $allocation) use ($server) {
|
||||
if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) {
|
||||
return $server->update(['allocation_id' => $allocation->id]);
|
||||
}
|
||||
})
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->label('Primary'),
|
||||
])
|
||||
->actions([
|
||||
DetachAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server))
|
||||
->label('Delete')
|
||||
->icon('tabler-trash')
|
||||
->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id)
|
||||
->action(function (Allocation $allocation) {
|
||||
Allocation::query()->where('id', $allocation->id)->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Actions\Action::make('addAllocation')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_CREATE, $server))
|
||||
->label(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'Allocation limit reached' : 'Add Allocation')
|
||||
->hidden(fn () => !config('panel.client_features.allocations.enabled'))
|
||||
->disabled(fn () => $server->allocations()->count() >= $server->allocation_limit)
|
||||
->color(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'danger' : 'primary')
|
||||
->action(function (FindAssignableAllocationService $service) use ($server) {
|
||||
$allocation = $service->handle($server);
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\BackupResource\Pages;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BackupResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Backup::class;
|
||||
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-file-zip';
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_BACKUP_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_BACKUP_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_BACKUP_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListBackups::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\BackupResource\Pages;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\BackupResource;
|
||||
use App\Http\Controllers\Api\Client\Servers\BackupController;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonBackupRepository;
|
||||
use App\Services\Backups\DownloadLinkService;
|
||||
use App\Services\Backups\InitiateBackupService;
|
||||
use App\Tables\Columns\BytesColumn;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ListBackups extends ListRecords
|
||||
{
|
||||
protected static string $resource = BackupResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->columnSpanFull()
|
||||
->required(),
|
||||
TextArea::make('ignored')
|
||||
->columnSpanFull()
|
||||
->label('Ignored Files & Directories'),
|
||||
Toggle::make('is_locked')
|
||||
->label('Lock?')
|
||||
->helperText('Prevents this backup from being deleted until explicitly unlocked.'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
BytesColumn::make('bytes')
|
||||
->label('Size'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->label('Created')
|
||||
->since()
|
||||
->sortable(),
|
||||
IconColumn::make('is_successful')
|
||||
->label('Successful')
|
||||
->boolean(),
|
||||
IconColumn::make('is_locked')
|
||||
->label('Lock Status')
|
||||
->icon(fn (Backup $backup) => !$backup->is_locked ? 'tabler-lock-open' : 'tabler-lock'),
|
||||
])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
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')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->toggleLock($request, $server, $backup)),
|
||||
Action::make('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),
|
||||
Action::make('restore')
|
||||
->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.'),
|
||||
Checkbox::make('truncate')
|
||||
->label('Delete all files before restoring backup?'),
|
||||
])
|
||||
->action(function (Backup $backup, $data, DaemonBackupRepository $daemonRepository, DownloadLinkService $downloadLinkService) use ($server) {
|
||||
if (!is_null($server->status)) {
|
||||
return Notification::make()
|
||||
->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)) { //TODO Change to Notifications
|
||||
return Notification::make()
|
||||
->danger()
|
||||
->title('Backup Restore Failed')
|
||||
->body('This backup cannot be restored at this time: not completed or failed.')
|
||||
->send();
|
||||
}
|
||||
|
||||
$log = Activity::event('server:backup.restore')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
|
||||
|
||||
$log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
|
||||
// If the backup is for an S3 file we need to generate a unique Download link for
|
||||
// it that will allow daemon to actually access the file.
|
||||
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
|
||||
$url = $downloadLinkService->handle($backup, auth()->user());
|
||||
}
|
||||
|
||||
// Update the status right away for the server so that we know not to allow certain
|
||||
// actions against it via the Panel API.
|
||||
$server->update(['status' => ServerState::RestoringBackup]);
|
||||
|
||||
$daemonRepository->setServer($server)->restore($backup, $url ?? null, $data['truncate']);
|
||||
});
|
||||
|
||||
return Notification::make()
|
||||
->title('Restoring Backup')
|
||||
->send();
|
||||
}),
|
||||
DeleteAction::make('delete')
|
||||
->disabled(fn (Backup $backup): bool => $backup->is_locked)
|
||||
->modalDescription(fn (Backup $backup) => 'Do you wish to delete, ' . $backup->name . '?')
|
||||
->modalSubmitActionLabel('Delete Backup')
|
||||
->action(fn (BackupController $backupController, Backup $backup, Request $request) => $backupController->delete($request, $server, $backup)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
Actions\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')
|
||||
->disabled(fn () => $server->backups()->count() >= $server->backup_limit)
|
||||
->color(fn () => $server->backups()->count() >= $server->backup_limit ? 'danger' : 'primary')
|
||||
->createAnother(false)
|
||||
->action(function (InitiateBackupService $initiateBackupService, $data) use ($server) {
|
||||
$action = $initiateBackupService->setIgnoredFiles(explode(PHP_EOL, $data['ignored'] ?? ''));
|
||||
|
||||
if (auth()->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
|
||||
$action->setIsLocked((bool) $data['is_locked']);
|
||||
}
|
||||
|
||||
$backup = $action->handle($server, $data['name']);
|
||||
|
||||
Activity::event('server:backup.start')
|
||||
->subject($backup)
|
||||
->property(['name' => $backup->name, 'locked' => (bool) $data['is_locked']])
|
||||
->log();
|
||||
|
||||
return Notification::make()
|
||||
->title('Backup Created')
|
||||
->body($backup->name . ' created.')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
use App\Models\Database;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DatabaseResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Database::class;
|
||||
|
||||
protected static ?int $navigationSort = 6;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canView(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_DATABASE_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListDatabases::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\DatabaseResource;
|
||||
use App\Models\Database;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class ListDatabases extends ListRecords
|
||||
{
|
||||
protected static string $resource = DatabaseResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('database')
|
||||
->columnSpanFull()
|
||||
->suffixAction(CopyAction::make()),
|
||||
TextInput::make('username')
|
||||
->suffixAction(CopyAction::make()),
|
||||
TextInput::make('password')
|
||||
->password()->revealable()
|
||||
->hidden(fn () => !auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->hintAction(
|
||||
Action::make('rotate')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_DATABASE_UPDATE, $server))
|
||||
->icon('tabler-refresh')
|
||||
->requiresConfirmation()
|
||||
->action(function (DatabasePasswordService $service, Database $database, $set, $get) {
|
||||
$newPassword = $service->handle($database);
|
||||
|
||||
$set('password', $newPassword);
|
||||
$set('JDBC', 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database'));
|
||||
})
|
||||
)
|
||||
->suffixAction(CopyAction::make())
|
||||
->formatStateUsing(fn (Database $database) => $database->password),
|
||||
TextInput::make('remote')
|
||||
->label('Connections From'),
|
||||
TextInput::make('max_connections')
|
||||
->formatStateUsing(fn (Database $database) => $database->max_connections === 0 ? $database->max_connections : 'Unlimited'),
|
||||
TextInput::make('JDBC')
|
||||
->label('JDBC Connection String')
|
||||
->password()->revealable()
|
||||
->hidden(!auth()->user()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $server))
|
||||
->suffixAction(CopyAction::make())
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('database'),
|
||||
TextColumn::make('username'),
|
||||
TextColumn::make('remote'),
|
||||
DateTimeColumn::make('created_at')
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make()
|
||||
->modalHeading(fn (Database $database) => 'Viewing ' . $database->database),
|
||||
DeleteAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
CreateAction::make('new')
|
||||
->label(fn () => $server->databases()->count() >= $server->database_limit ? 'Database limit reached' : 'Create Database')
|
||||
->disabled(fn () => $server->databases()->count() >= $server->database_limit)
|
||||
->color(fn () => $server->databases()->count() >= $server->database_limit ? 'danger' : 'primary')
|
||||
->createAnother(false)
|
||||
->form([
|
||||
Grid::make()
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextInput::make('database')
|
||||
->columnSpan(2)
|
||||
->label('Database Name')
|
||||
->prefix('s'. $server->id . '_')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Leaving this blank will auto generate a random name'),
|
||||
TextInput::make('remote')
|
||||
->columnSpan(1)
|
||||
->label('Connections From')
|
||||
->default('%'),
|
||||
]),
|
||||
])
|
||||
->action(function ($data, DatabaseManagementService $service) use ($server) {
|
||||
if (empty($data['database'])) {
|
||||
$data['database'] = str_random(12);
|
||||
}
|
||||
|
||||
$data['database_host_id'] = DatabaseHost::where('node_id', $server->node_id)->first()->id;
|
||||
$data['database'] = 's'. $server->id . '_' . $data['database'];
|
||||
|
||||
$service->create($server, $data);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\FileResource\Pages;
|
||||
use App\Models\File;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FileResource extends Resource
|
||||
{
|
||||
protected static ?string $model = File::class;
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-files';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_FILE_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_FILE_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_FILE_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_FILE_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'edit' => Pages\EditFiles::route('/edit/{path}'),
|
||||
'search' => Pages\SearchFiles::route('/search/{searchTerm}'), // TODO: find better way?
|
||||
'index' => Pages\ListFiles::route('/{path?}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\FileResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Enums\EditorLanguages;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Models\File;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\Concerns\InteractsWithFormActions;
|
||||
use Filament\Panel;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class EditFiles extends Page
|
||||
{
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithFormActions;
|
||||
use InteractsWithForms;
|
||||
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
protected static string $view = 'filament.server.pages.edit-file';
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
File::get($server, dirname($this->path))->orderByDesc('is_directory')->orderBy('name');
|
||||
|
||||
return $form
|
||||
->schema([
|
||||
Select::make('lang')
|
||||
->live()
|
||||
->label('')
|
||||
->placeholder('File Language')
|
||||
->options(EditorLanguages::class)
|
||||
->hidden() //TODO Fix Dis
|
||||
->default(function () {
|
||||
$split = explode('.', $this->path);
|
||||
|
||||
return end($split);
|
||||
}),
|
||||
Section::make('Editing: ' . $this->path)
|
||||
->footerActions([
|
||||
Action::make('save')
|
||||
->label('Save Changes')
|
||||
->authorize(auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->icon('tabler-device-floppy')
|
||||
->keyBindings('mod+s')
|
||||
->action(function () use ($server) {
|
||||
$data = $this->form->getState();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->putContent($this->path, $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', $this->path)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->duration(5000) // 5 seconds
|
||||
->title('Saved File')
|
||||
->body(fn () => $this->path)
|
||||
->send();
|
||||
}),
|
||||
Action::make('cancel')
|
||||
->label('Cancel')
|
||||
->color('danger')
|
||||
->icon('tabler-x')
|
||||
->url(fn () => ListFiles::getUrl(['path' => dirname($this->path)])),
|
||||
])
|
||||
->footerActionsAlignment(Alignment::End)
|
||||
->schema([
|
||||
MonacoEditor::make('editor')
|
||||
->label('')
|
||||
->formatStateUsing(function () use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
return app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->getContent($this->path, config('panel.files.max_edit_size'));
|
||||
})
|
||||
->language(fn (Get $get) => $get('lang') ?? 'plaintext')
|
||||
->view('filament.plugins.monaco-editor'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function mount(string $path): void
|
||||
{
|
||||
$this->authorizeAccess();
|
||||
|
||||
$this->path = $path;
|
||||
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
{
|
||||
abort_unless(auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, Filament::getTenant()), 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int | string, string | Form>
|
||||
*/
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->form(static::getResource()::form(
|
||||
$this->makeForm()
|
||||
->statePath($this->getFormStatePath())
|
||||
->columns($this->hasInlineLabels() ? 1 : 2)
|
||||
->inlineLabel($this->hasInlineLabels()),
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
$resource = static::getResource();
|
||||
|
||||
$breadcrumbs = [
|
||||
$resource::getUrl() => $resource::getBreadcrumb(),
|
||||
];
|
||||
|
||||
$previousParts = '';
|
||||
foreach (explode('/', $this->path) as $part) {
|
||||
$previousParts = $previousParts . '/' . $part;
|
||||
$breadcrumbs[self::getUrl(['path' => ltrim($previousParts, '/')])] = $part;
|
||||
}
|
||||
|
||||
return $breadcrumbs;
|
||||
}
|
||||
|
||||
public static function route(string $path): PageRegistration
|
||||
{
|
||||
return new PageRegistration(
|
||||
page: static::class,
|
||||
route: fn (Panel $panel): Route => RouteFacade::get($path, static::class)
|
||||
->middleware(static::getRouteMiddleware($panel))
|
||||
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
|
||||
->where('path', '.*'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,588 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\FileResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Enums\EditorLanguages;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Models\File;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use App\Tables\Columns\BytesColumn;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Filament\Actions\Action as HeaderAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Panel;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Resources\Pages\PageRegistration;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkAction;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
class ListFiles extends ListRecords
|
||||
{
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public function mount(?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
$this->path = $path ?? '/';
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
$resource = static::getResource();
|
||||
|
||||
$breadcrumbs = [
|
||||
$resource::getUrl() => $resource::getBreadcrumb(),
|
||||
];
|
||||
|
||||
$previousParts = '';
|
||||
foreach (explode('/', $this->path) as $part) {
|
||||
$previousParts = $previousParts . '/' . $part;
|
||||
$breadcrumbs[self::getUrl(['path' => ltrim($previousParts, '/')])] = $part;
|
||||
}
|
||||
|
||||
return $breadcrumbs;
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated([15, 25, 50, 100])
|
||||
->defaultPaginationPageOption(15)
|
||||
->query(fn () => File::get($server, $this->path)->orderByDesc('is_directory')->orderBy('name'))
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->icon(fn (File $file) => $file->getIcon()),
|
||||
BytesColumn::make('size'),
|
||||
DateTimeColumn::make('modified_at')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->recordUrl(function (File $file) use ($server) {
|
||||
|
||||
if ($file->is_directory) {
|
||||
return self::getUrl(['path' => join_paths($this->path, $file->name)]);
|
||||
}
|
||||
|
||||
if (!auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
|
||||
})
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->label('Open')
|
||||
->icon('tabler-eye')
|
||||
->visible(fn (File $file) => $file->is_directory)
|
||||
->url(fn (File $file) => self::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
EditAction::make('edit')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->label('Edit')
|
||||
->icon('tabler-edit')
|
||||
->visible(fn (File $file) => $file->canEdit())
|
||||
->url(fn (File $file) => EditFiles::getUrl(['path' => join_paths($this->path, $file->name)])),
|
||||
ActionGroup::make([
|
||||
Action::make('rename')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Rename')
|
||||
->icon('tabler-forms')
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('File name')
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required(),
|
||||
])
|
||||
->action(function ($data, File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, [['to' => $data['name'], 'from' => $file->name]]);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', [['to' => $data['name'], 'from' => $file->name]])
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File Renamed')
|
||||
->body(fn () => $file->name . ' -> ' . $data['name'])
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('copy')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('Copy')
|
||||
->icon('tabler-copy')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->copyFile(join_paths($this->path, $file->name));
|
||||
|
||||
Activity::event('server:file.copy')
|
||||
->property('file', join_paths($this->path, $file->name))
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('File copied')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
Action::make('download')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ_CONTENT, $server))
|
||||
->label('Download')
|
||||
->icon('tabler-download')
|
||||
->visible(fn (File $file) => $file->is_file)
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
$token = app(NodeJWTService::class)
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setUser(auth()->user())
|
||||
->setClaims([
|
||||
'file_path' => rawurldecode(join_paths($this->path, $file->name)),
|
||||
'server_uuid' => $server->uuid,
|
||||
])
|
||||
->handle($server->node, auth()->user()->id . $server->uuid);
|
||||
|
||||
Activity::event('server:file.download')
|
||||
->property('file', join_paths($this->path, $file->name))
|
||||
->log();
|
||||
|
||||
return redirect()->away(sprintf('%s/download/file?token=%s', $server->node->getConnectionAddress(), $token->toString())); // TODO: download works, but breaks modals
|
||||
}),
|
||||
Action::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Move')
|
||||
->icon('tabler-replace')
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('File name')
|
||||
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location')))),
|
||||
])
|
||||
->action(function ($data, File $file) use ($server) {
|
||||
$location = resolve_path(join_paths($this->path, $data['location']));
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, [['to' => $location, 'from' => $file->name]]);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', [['to' => $location, 'from' => $file->name]])
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(join_paths($this->path, $file->name) . ' was moved to ' . $location)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('permissions')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->label('Permissions')
|
||||
->icon('tabler-license')
|
||||
->form([
|
||||
CheckboxList::make('owner')
|
||||
->bulkToggleable()
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 0, 1);
|
||||
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
CheckboxList::make('group')
|
||||
->bulkToggleable()
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 1, 1);
|
||||
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
CheckboxList::make('public')
|
||||
->bulkToggleable()
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'write' => 'Write',
|
||||
'execute' => 'Execute',
|
||||
])
|
||||
->formatStateUsing(function ($state, File $file) {
|
||||
$mode = (int) substr((string) $file->mode_bits, 2, 1);
|
||||
|
||||
return $this->getPermissionsFromModeBit($mode);
|
||||
}),
|
||||
])
|
||||
->action(function ($data, File $file) use ($server) {
|
||||
$owner = (in_array('read', $data['owner']) ? 4 : 0) | (in_array('write', $data['owner']) ? 2 : 0) | (in_array('execute', $data['owner']) ? 1 : 0);
|
||||
$group = (in_array('read', $data['group']) ? 4 : 0) | (in_array('write', $data['group']) ? 2 : 0) | (in_array('execute', $data['group']) ? 1 : 0);
|
||||
$public = (in_array('read', $data['public']) ? 4 : 0) | (in_array('write', $data['public']) ? 2 : 0) | (in_array('execute', $data['public']) ? 1 : 0);
|
||||
|
||||
$mode = $owner . $group . $public;
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->chmodFiles($this->path, [['file' => $file->name, 'mode' => $mode]]);
|
||||
|
||||
Notification::make()
|
||||
->title('Permissions changed to ' . $mode)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->label('Archive')
|
||||
->icon('tabler-archive')
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->compressFiles($this->path, [$file->name]);
|
||||
|
||||
Activity::event('server:file.compress')
|
||||
->property('directory', $this->path)
|
||||
->property('files', [$file->name])
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
Action::make('unarchive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->label('Unarchive')
|
||||
->icon('tabler-archive')
|
||||
->visible(fn (File $file) => $file->isArchive())
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->decompressFile($this->path, $file->name);
|
||||
|
||||
Activity::event('server:file.decompress')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $file->name)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Unarchive completed')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
]),
|
||||
DeleteAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->label('')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->modalDescription(fn (File $file) => $file->name)
|
||||
->modalHeading('Delete file?')
|
||||
->action(function (File $file) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->deleteFiles($this->path, [$file->name]);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $file->name)
|
||||
->log();
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
BulkAction::make('move')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_UPDATE, $server))
|
||||
->form([
|
||||
TextInput::make('location')
|
||||
->label('File name')
|
||||
->hint('Enter the new name and directory of this file or folder, relative to the current directory.')
|
||||
->default(fn (File $file) => $file->name)
|
||||
->required()
|
||||
->live(),
|
||||
Placeholder::make('new_location')
|
||||
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
|
||||
])
|
||||
->action(function (Collection $files, $data) use ($server) {
|
||||
$location = resolve_path(join_paths($this->path, $data['location']));
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$files = $files->map(fn ($file) => ['to' => $location, 'from' => $file->name])->toArray();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->renameFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.rename')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files were moved from to ' . $location)
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
BulkAction::make('archive')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_ARCHIVE, $server))
|
||||
->action(function (Collection $files) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
$files = $files->map(fn ($file) => $file->name)->toArray();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->compressFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.compress')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title('Archive created')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
}),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_DELETE, $server))
|
||||
->action(function (Collection $files) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
$files = $files->map(fn ($file) => $file->name)->toArray();
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->deleteFiles($this->path, $files);
|
||||
|
||||
Activity::event('server:file.delete')
|
||||
->property('directory', $this->path)
|
||||
->property('files', $files)
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(count($files) . ' Files deleted.')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return [
|
||||
HeaderAction::make('new_file')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('New File')
|
||||
->color('gray')
|
||||
->keyBindings('')
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function ($data) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->putContent(join_paths($this->path, $data['name']), $data['editor'] ?? '');
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', join_paths($this->path, $data['name']))
|
||||
->log();
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('File Name')
|
||||
->required(),
|
||||
Select::make('lang')
|
||||
->live()
|
||||
->hidden() //TODO: Make file language selection work
|
||||
->label('Language')
|
||||
->placeholder('File Language')
|
||||
->options(EditorLanguages::class),
|
||||
MonacoEditor::make('editor')
|
||||
->label('')
|
||||
->view('filament.plugins.monaco-editor')
|
||||
->language(fn (Get $get) => $get('lang')),
|
||||
]),
|
||||
HeaderAction::make('new_folder')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('New Folder')
|
||||
->color('gray')
|
||||
->action(function ($data) use ($server) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->createDirectory($data['name'], $this->path);
|
||||
|
||||
Activity::event('server:file.write')
|
||||
->property('file', join_paths($this->path, $data['name']))
|
||||
->log();
|
||||
})
|
||||
->form([
|
||||
TextInput::make('name')
|
||||
->label('Folder Name')
|
||||
->required(),
|
||||
]),
|
||||
HeaderAction::make('upload')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_CREATE, $server))
|
||||
->label('Upload')
|
||||
->action(function ($data) use ($server) {
|
||||
if (count($data['files']) > 0 && !isset($data['url'])) {
|
||||
/** @var UploadedFile $file */
|
||||
foreach ($data['files'] as $file) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->putContent(join_paths($this->path, $file->getClientOriginalName()), $file->getContent());
|
||||
|
||||
Activity::event('server:file.uploaded')
|
||||
->property('directory', $this->path)
|
||||
->property('file', $file->getFilename())
|
||||
->log();
|
||||
}
|
||||
} elseif ($data['url'] !== null) {
|
||||
// @phpstan-ignore-next-line
|
||||
app(DaemonFileRepository::class)
|
||||
->setServer($server)
|
||||
->pull($data['url'], $this->path);
|
||||
|
||||
Activity::event('server:file.pull')
|
||||
->property('url', $data['url'])
|
||||
->property('directory', $this->path)
|
||||
->log();
|
||||
}
|
||||
|
||||
return redirect(ListFiles::getUrl(['path' => $this->path]));
|
||||
|
||||
})
|
||||
->form([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->schema([
|
||||
Tabs\Tab::make('Upload Files')
|
||||
->live()
|
||||
->schema([
|
||||
FileUpload::make('files')
|
||||
->label('File(s)')
|
||||
->storeFiles(false)
|
||||
->previewable(false)
|
||||
->preserveFilenames()
|
||||
->multiple(),
|
||||
]),
|
||||
Tabs\Tab::make('Upload From URL')
|
||||
->live()
|
||||
->disabled(fn (Get $get) => count($get('files')) > 0)
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->url(),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
HeaderAction::make('search')
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_FILE_READ, $server))
|
||||
->label('Global Search')
|
||||
->modalSubmitActionLabel('Search')
|
||||
->form([
|
||||
TextInput::make('searchTerm')
|
||||
->placeholder('Enter a search term, e.g. *.txt')
|
||||
->minLength(3),
|
||||
])
|
||||
->action(fn ($data) => redirect(SearchFiles::getUrl([
|
||||
'searchTerm' => $data['searchTerm'],
|
||||
'path' => $this->path,
|
||||
]))),
|
||||
];
|
||||
}
|
||||
|
||||
public static function route(string $path): PageRegistration
|
||||
{
|
||||
return new PageRegistration(
|
||||
page: static::class,
|
||||
route: fn (Panel $panel): Route => RouteFacade::get($path, static::class)
|
||||
->middleware(static::getRouteMiddleware($panel))
|
||||
->withoutMiddleware(static::getWithoutRouteMiddleware($panel))
|
||||
->where('path', '.*'),
|
||||
);
|
||||
}
|
||||
|
||||
private function getPermissionsFromModeBit(int $mode): array
|
||||
{
|
||||
if ($mode === 1) {
|
||||
return ['execute'];
|
||||
} elseif ($mode === 2) {
|
||||
return ['write'];
|
||||
} elseif ($mode === 3) {
|
||||
return ['write', 'execute'];
|
||||
} elseif ($mode === 4) {
|
||||
return ['read'];
|
||||
} elseif ($mode === 5) {
|
||||
return ['read', 'execute'];
|
||||
} elseif ($mode === 6) {
|
||||
return ['read', 'write'];
|
||||
} elseif ($mode === 7) {
|
||||
return ['read', 'write', 'execute'];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\FileResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\FileResource;
|
||||
use App\Models\File;
|
||||
use App\Models\Server;
|
||||
use App\Tables\Columns\BytesColumn;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
class SearchFiles extends ListRecords
|
||||
{
|
||||
protected static string $resource = FileResource::class;
|
||||
|
||||
protected static ?string $title = 'Global Search';
|
||||
|
||||
#[Locked]
|
||||
public string $searchTerm;
|
||||
|
||||
#[Locked]
|
||||
public string $path;
|
||||
|
||||
public function mount(?string $searchTerm = null, ?string $path = null): void
|
||||
{
|
||||
parent::mount();
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->path = $path ?? '/';
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
$resource = static::getResource();
|
||||
|
||||
return [
|
||||
$resource::getUrl() => $resource::getBreadcrumb(),
|
||||
self::getUrl(['searchTerm' => $this->searchTerm]) => 'Search "' . $this->searchTerm . '"',
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated(false)
|
||||
->query(fn () => File::get($server, $this->path, $this->searchTerm)->orderByDesc('is_directory')->orderBy('name'))
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->icon(fn (File $file) => $file->getIcon()),
|
||||
BytesColumn::make('size'),
|
||||
DateTimeColumn::make('modified_at')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->recordUrl(function (File $file) {
|
||||
if ($file->is_directory) {
|
||||
return ListFiles::getUrl(['path' => join_paths($this->path, $file->name)]);
|
||||
}
|
||||
|
||||
return $file->canEdit() ? EditFiles::getUrl(['path' => join_paths($this->path, $file->name)]) : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
use App\Filament\Server\Resources\ScheduleResource\RelationManagers\TasksRelationManager;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Server;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScheduleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Schedule::class;
|
||||
|
||||
protected static ?int $navigationSort = 4;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-clock';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_SCHEDULE_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_SCHEDULE_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_SCHEDULE_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_SCHEDULE_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->columns(10)
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->columnSpan(10)
|
||||
->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.')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->columnSpan(5)
|
||||
->required()
|
||||
->default(1),
|
||||
Toggle::make('is_active')
|
||||
->label('Enable Schedule?')
|
||||
->hintIconTooltip('This schedule will be executed automatically if enabled.')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->columnSpan(5)
|
||||
->required()
|
||||
->default(1),
|
||||
TextInput::make('cron_minute')
|
||||
->columnSpan(2)
|
||||
->label('Minute')
|
||||
->default('*/5')
|
||||
->required(),
|
||||
TextInput::make('cron_hour')
|
||||
->columnSpan(2)
|
||||
->label('Hour')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_day_of_month')
|
||||
->columnSpan(2)
|
||||
->label('Day of Month')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_month')
|
||||
->columnSpan(2)
|
||||
->label('Month')
|
||||
->default('*')
|
||||
->required(),
|
||||
TextInput::make('cron_day_of_week')
|
||||
->columnSpan(2)
|
||||
->label('Day of Week')
|
||||
->default('*')
|
||||
->required(),
|
||||
Section::make('Presets')
|
||||
->schema([
|
||||
Actions::make([
|
||||
Action::make('hourly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '*');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('daily')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('weekly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '0');
|
||||
}),
|
||||
Action::make('monthly')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->action(function (Set $set) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '1');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '0');
|
||||
}),
|
||||
Action::make('every_x_minutes')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(60)
|
||||
->prefix('Every')
|
||||
->suffix('Minutes'),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '*/' . $data['x']);
|
||||
$set('cron_hour', '*');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_hours')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Hours'),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '*/' . $data['x']);
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_days')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Days'),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*/' . $data['x']);
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_months')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
TextInput::make('x')
|
||||
->label('')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(24)
|
||||
->prefix('Every')
|
||||
->suffix('Months'),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '0');
|
||||
$set('cron_month', '*/' . $data['x']);
|
||||
$set('cron_day_of_week', '*');
|
||||
}),
|
||||
Action::make('every_x_day_of_week')
|
||||
->disabled(fn (string $operation) => $operation === 'view')
|
||||
->form([
|
||||
Select::make('x')
|
||||
->label('')
|
||||
->prefix('Every')
|
||||
->options([
|
||||
'0' => 'Sunday',
|
||||
'1' => 'Monday',
|
||||
'2' => 'Tuesday',
|
||||
'3' => 'Wednesday',
|
||||
'4' => 'Thursday',
|
||||
'5' => 'Friday',
|
||||
'6' => 'Saturday',
|
||||
]),
|
||||
])
|
||||
->action(function (Set $set, $data) {
|
||||
$set('cron_minute', '0');
|
||||
$set('cron_hour', '0');
|
||||
$set('cron_day_of_month', '*');
|
||||
$set('cron_month', '*');
|
||||
$set('cron_day_of_week', $data['x']);
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
TasksRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListSchedules::route('/'),
|
||||
'create' => Pages\CreateSchedule::route('/create'),
|
||||
'view' => Pages\ViewSchedule::route('/{record}'),
|
||||
'edit' => Pages\EditSchedule::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Helpers\Utilities;
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateSchedule extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (!isset($data['server_id'])) {
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
$data['server_id'] = $server->id;
|
||||
}
|
||||
|
||||
if (!isset($data['next_run_at'])) {
|
||||
$data['next_run_at'] = $this->getNextRunAt($data['cron_minute'], $data['cron_hour'], $data['cron_day_of_month'], $data['cron_month'], $data['cron_day_of_week']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getNextRunAt(string $minute, string $hour, string $dayOfMonth, string $month, string $dayOfWeek): Carbon
|
||||
{
|
||||
try {
|
||||
return Utilities::getScheduleNextRunDate(
|
||||
$minute,
|
||||
$hour,
|
||||
$dayOfMonth,
|
||||
$month,
|
||||
$dayOfWeek
|
||||
);
|
||||
} catch (Exception) {
|
||||
throw new DisplayException('The cron data provided does not evaluate to a valid expression.');
|
||||
}
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditSchedule extends EditRecord
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use App\Models\Schedule;
|
||||
use App\Tables\Columns\DateTimeColumn;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListSchedules extends ListRecords
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('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')),
|
||||
IconColumn::make('only_when_online')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('last_run_at')
|
||||
->label('Last run')
|
||||
->since()
|
||||
->sortable(),
|
||||
DateTimeColumn::make('next_run_at')
|
||||
->label('Next run')
|
||||
->since()
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\Pages;
|
||||
|
||||
use App\Filament\Server\Resources\ScheduleResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewSchedule extends ViewRecord
|
||||
{
|
||||
protected static string $resource = ScheduleResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getBreadcrumbs(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources\ScheduleResource\RelationManagers;
|
||||
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Task;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
|
||||
class TasksRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'tasks';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
/** @var Schedule $schedule */
|
||||
$schedule = $this->getOwnerRecord();
|
||||
|
||||
return $table
|
||||
->reorderable('sequence_id', true)
|
||||
->columns([
|
||||
TextColumn::make('action')
|
||||
->state(fn (Task $task) => match ($task->action) {
|
||||
Task::ACTION_POWER => 'Send power action',
|
||||
Task::ACTION_COMMAND => 'Send command',
|
||||
Task::ACTION_BACKUP => 'Create backup',
|
||||
Task::ACTION_DELETE_FILES => 'Delete files',
|
||||
default => $task->action
|
||||
}),
|
||||
TextColumn::make('time_offset')
|
||||
->hidden(fn () => config('queue.default') === 'sync')
|
||||
->suffix(' Seconds'),
|
||||
IconColumn::make('continue_on_failure')
|
||||
->boolean(),
|
||||
])
|
||||
->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')
|
||||
->disabled(fn () => $schedule->tasks()->count() >= config('panel.client_features.schedules.per_schedule_task_limit', 10))
|
||||
->form([
|
||||
Select::make('action')
|
||||
->required()
|
||||
->live()
|
||||
->disableOptionWhen(fn (string $value): bool => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0)
|
||||
->options([
|
||||
Task::ACTION_POWER => 'Send power action',
|
||||
Task::ACTION_COMMAND => 'Send command',
|
||||
Task::ACTION_BACKUP => 'Create backup',
|
||||
Task::ACTION_DELETE_FILES => 'Delete files',
|
||||
]),
|
||||
Textarea::make('payload')
|
||||
->hidden(fn (Get $get) => $get('action') === Task::ACTION_POWER)
|
||||
->label(fn (Get $get) => match ($get('action')) {
|
||||
Task::ACTION_POWER => 'Power action',
|
||||
Task::ACTION_COMMAND => 'Command',
|
||||
Task::ACTION_BACKUP => 'Files to ignore',
|
||||
Task::ACTION_DELETE_FILES => 'Files to delete',
|
||||
default => 'Payload'
|
||||
}),
|
||||
Select::make('payload')
|
||||
->visible(fn (Get $get) => $get('action') === Task::ACTION_POWER)
|
||||
->label('Power Action')
|
||||
->required()
|
||||
->options([
|
||||
'start' => 'Start',
|
||||
'restart' => 'Restart',
|
||||
'stop' => 'Stop',
|
||||
'Kill' => 'Kill',
|
||||
]),
|
||||
TextInput::make('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'),
|
||||
])
|
||||
->action(function ($data) use ($schedule) {
|
||||
$sequenceId = ($schedule->tasks()->orderByDesc('sequence_id')->first()->sequence_id ?? 0) + 1;
|
||||
|
||||
$task = Task::query()->create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'sequence_id' => $sequenceId,
|
||||
'action' => $data['action'],
|
||||
'payload' => $data['payload'] ?? '',
|
||||
'time_offset' => $data['time_offset'] ?? 0,
|
||||
'continue_on_failure' => (bool) $data['continue_on_failure'],
|
||||
]);
|
||||
|
||||
Activity::event('server:task.create')
|
||||
->subject($schedule, $task)
|
||||
->property(['name' => $schedule->name, 'action' => $task->action, 'payload' => $task->payload])
|
||||
->log();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Resources;
|
||||
|
||||
use App\Filament\Server\Resources\UserResource\Pages;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Models\Subuser;
|
||||
use App\Models\User;
|
||||
use App\Services\Subusers\SubuserDeletionService;
|
||||
use App\Services\Subusers\SubuserUpdateService;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserResource extends Resource
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-users';
|
||||
|
||||
protected static ?string $tenantOwnershipRelationshipName = 'subServers';
|
||||
|
||||
// TODO: find better way handle server conflict state
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if ($server->isInConflictState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::canAccess();
|
||||
}
|
||||
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_USER_READ, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_USER_CREATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_USER_UPDATE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function canDelete(Model $record): bool
|
||||
{
|
||||
return auth()->user()->can(Permission::ACTION_USER_DELETE, Filament::getTenant());
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
return $table
|
||||
->paginated(false)
|
||||
->searchable(false)
|
||||
->columns([
|
||||
ImageColumn::make('picture')
|
||||
->visibleFrom('lg')
|
||||
->label('')
|
||||
->extraImgAttributes(['class' => 'rounded-full'])
|
||||
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
->searchable(),
|
||||
TextColumn::make('permissions')
|
||||
->state(fn (User $user) => count(Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first()->permissions)),
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make()
|
||||
->label('Remove User')
|
||||
->hidden(fn (User $user) => auth()->user()->id === $user->id)
|
||||
->action(function (User $user, SubuserDeletionService $subuserDeletionService) use ($server) {
|
||||
$subuser = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first();
|
||||
$subuserDeletionService->handle($subuser, $server);
|
||||
|
||||
}),
|
||||
EditAction::make()
|
||||
->label('Edit User')
|
||||
|
||||
->authorize(fn () => auth()->user()->can(Permission::ACTION_USER_UPDATE, $server))
|
||||
->modalHeading(fn (User $user) => 'Editing ' . $user->email)
|
||||
->action(function (array $data, SubuserUpdateService $subuserUpdateService, User $user) use ($server) {
|
||||
$subuser = Subuser::query()->where('user_id', $user->id)->where('server_id', $server->id)->first();
|
||||
|
||||
if (in_array('console', $data['control'])) {
|
||||
$data['websocket'][0] = 'connect';
|
||||
}
|
||||
|
||||
$permissions = collect($data)->forget('email')->map(fn ($permissions, $key) => collect($permissions)->map(fn ($permission) => "$key.$permission"))->flatten()->all();
|
||||
$subuserUpdateService->handle($subuser, $server, $permissions);
|
||||
|
||||
Notification::make()
|
||||
->title('User Updated!')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return redirect(self::getUrl(tenant: $server));
|
||||
})
|
||||
->form([
|
||||
Grid::make()
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 5,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('email')
|
||||
->inlineLabel()
|
||||
->disabled()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 4,
|
||||
'lg' => 5,
|
||||
]),
|
||||
Actions::make([
|
||||
Action::make('assignAll')
|
||||
->label('Assign All')
|
||||
->action(function (Set $set) {
|
||||
$permissions = [
|
||||
'control' => [
|
||||
'console',
|
||||
'start',
|
||||
'stop',
|
||||
'restart',
|
||||
],
|
||||
'user' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'file' => [
|
||||
'read',
|
||||
'read-content',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'archive',
|
||||
'sftp',
|
||||
],
|
||||
'backup' => [
|
||||
'read',
|
||||
'create',
|
||||
'delete',
|
||||
'download',
|
||||
'restore',
|
||||
],
|
||||
'allocation' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'startup' => [
|
||||
'read',
|
||||
'update',
|
||||
'docker-image',
|
||||
],
|
||||
'database' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'view_password',
|
||||
],
|
||||
'schedule' => [
|
||||
'read',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
'settings' => [
|
||||
'rename',
|
||||
'reinstall',
|
||||
'activity',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($permissions as $key => $value) {
|
||||
$allValues = array_unique($value);
|
||||
$set($key, $allValues);
|
||||
}
|
||||
}),
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
Tabs::make()
|
||||
->columnSpanFull()
|
||||
->schema([
|
||||
Tab::make('Console')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.control_desc'))
|
||||
->icon('tabler-terminal-2')
|
||||
->schema([
|
||||
CheckboxList::make('control')
|
||||
->formatStateUsing(function (User $user, Set $set) use ($server) {
|
||||
$permissionsArray = Subuser::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('server_id', $server->id)
|
||||
->first()
|
||||
->permissions;
|
||||
|
||||
$transformedPermissions = [];
|
||||
|
||||
foreach ($permissionsArray as $permission) {
|
||||
[$group, $action] = explode('.', $permission, 2);
|
||||
$transformedPermissions[$group][] = $action;
|
||||
}
|
||||
|
||||
foreach ($transformedPermissions as $key => $value) {
|
||||
$set($key, $value);
|
||||
}
|
||||
|
||||
return $transformedPermissions['control'] ?? [];
|
||||
})
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'console' => 'Console',
|
||||
'start' => 'Start',
|
||||
'stop' => 'Stop',
|
||||
'restart' => 'Restart',
|
||||
])
|
||||
->descriptions([
|
||||
'console' => trans('server/users.permissions.control_console'),
|
||||
'start' => trans('server/users.permissions.control_start'),
|
||||
'stop' => trans('server/users.permissions.control_stop'),
|
||||
'restart' => trans('server/users.permissions.control_restart'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('User')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.user_desc'))
|
||||
->icon('tabler-users')
|
||||
->schema([
|
||||
CheckboxList::make('user')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.user_create'),
|
||||
'read' => trans('server/users.permissions.user_read'),
|
||||
'update' => trans('server/users.permissions.user_update'),
|
||||
'delete' => trans('server/users.permissions.user_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('File')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.file_desc'))
|
||||
->icon('tabler-folders')
|
||||
->schema([
|
||||
CheckboxList::make('file')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'read-content' => 'Read Content',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'archive' => 'Archive',
|
||||
'sftp' => 'SFTP',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.file_create'),
|
||||
'read' => trans('server/users.permissions.file_read'),
|
||||
'read-content' => trans('server/users.permissions.file_read_content'),
|
||||
'update' => trans('server/users.permissions.file_update'),
|
||||
'delete' => trans('server/users.permissions.file_delete'),
|
||||
'archive' => trans('server/users.permissions.file_archive'),
|
||||
'sftp' => trans('server/users.permissions.file_sftp'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Backup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.backup_desc'))
|
||||
->icon('tabler-download')
|
||||
->schema([
|
||||
CheckboxList::make('backup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'delete' => 'Delete',
|
||||
'download' => 'Download',
|
||||
'restore' => 'Restore',
|
||||
])
|
||||
->descriptions([
|
||||
'create' => trans('server/users.permissions.backup_create'),
|
||||
'read' => trans('server/users.permissions.backup_read'),
|
||||
'delete' => trans('server/users.permissions.backup_delete'),
|
||||
'download' => trans('server/users.permissions.backup_download'),
|
||||
'restore' => trans('server/users.permissions.backup_restore'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Allocation')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.allocation_desc'))
|
||||
->icon('tabler-network')
|
||||
->schema([
|
||||
CheckboxList::make('allocation')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.allocation_read'),
|
||||
'create' => trans('server/users.permissions.allocation_create'),
|
||||
'update' => trans('server/users.permissions.allocation_update'),
|
||||
'delete' => trans('server/users.permissions.allocation_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Startup')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.startup_desc'))
|
||||
->icon('tabler-question-mark')
|
||||
->schema([
|
||||
CheckboxList::make('startup')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'update' => 'Update',
|
||||
'docker-image' => 'Docker Image',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.startup_read'),
|
||||
'update' => trans('server/users.permissions.startup_update'),
|
||||
'docker-image' => trans('server/users.permissions.startup_docker_image'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Database')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.database_desc'))
|
||||
->icon('tabler-database')
|
||||
->schema([
|
||||
CheckboxList::make('database')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
'view_password' => 'View Password',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.database_read'),
|
||||
'create' => trans('server/users.permissions.database_create'),
|
||||
'update' => trans('server/users.permissions.database_update'),
|
||||
'delete' => trans('server/users.permissions.database_delete'),
|
||||
'view_password' => trans('server/users.permissions.database_view_password'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Schedule')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.schedule_desc'))
|
||||
->icon('tabler-clock')
|
||||
->schema([
|
||||
CheckboxList::make('schedule')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'read' => 'Read',
|
||||
'create' => 'Create',
|
||||
'update' => 'Update',
|
||||
'delete' => 'Delete',
|
||||
])
|
||||
->descriptions([
|
||||
'read' => trans('server/users.permissions.schedule_read'),
|
||||
'create' => trans('server/users.permissions.schedule_create'),
|
||||
'update' => trans('server/users.permissions.schedule_update'),
|
||||
'delete' => trans('server/users.permissions.schedule_delete'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Settings')
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/users.permissions.settings_desc'))
|
||||
->icon('tabler-settings')
|
||||
->schema([
|
||||
CheckboxList::make('settings')
|
||||
->bulkToggleable()
|
||||
->label('')
|
||||
->options([
|
||||
'rename' => 'Rename',
|
||||
'reinstall' => 'Reinstall',
|
||||
'activity' => 'Activity',
|
||||
])
|
||||
->descriptions([
|
||||
'rename' => trans('server/users.permissions.setting_rename'),
|
||||
'reinstall' => trans('server/users.permissions.setting_reinstall'),
|
||||
'activity' => trans('server/users.permissions.setting_activity'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user