Update web installer (#614)

* update web installer

* make sure we have a user

* save SESSION_SECURE_COOKIE as text so it's written correctly to the .env

* set `SESSION_COOKIE` so session doesn't expire when changing the app name

* Allow enter to go to next step

---------

Co-authored-by: notCharles <charles@pelican.dev>
This commit is contained in:
Boy132 2024-10-12 16:19:52 +02:00 committed by GitHub
parent 3c5da1cd70
commit c0eedc16e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 251 additions and 184 deletions

View File

@ -4,6 +4,7 @@ APP_KEY=
APP_TIMEZONE=UTC
APP_URL=http://panel.test
APP_LOCALE=en
APP_INSTALLED=false
LOG_CHANNEL=daily
LOG_STACK=single
@ -30,3 +31,4 @@ MAIL_FROM_NAME="Pelican Admin"
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
SESSION_COOKIE=pelican_session

View File

@ -2,20 +2,14 @@
namespace App\Console\Commands\Environment;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command
{
use EnvironmentWriterTrait;
protected $description = 'Configure basic environment settings for the Panel.';
protected $signature = 'p:environment:setup
{--url= : The URL that this Panel is running on.}';
protected array $variables = [];
protected $signature = 'p:environment:setup';
public function handle(): void
{
@ -29,22 +23,5 @@ class AppSettingsCommand extends Command
$this->comment('Generating app key');
Artisan::call('key:generate');
}
$this->variables['APP_TIMEZONE'] = 'UTC';
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
'Application URL',
config('app.url', 'https://example.com')
);
// Make sure session cookies are set as "secure" when using HTTPS
if (str_starts_with($this->variables['APP_URL'], 'https://')) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
}
$this->comment('Writing variables to .env file');
$this->writeToEnvironment($this->variables);
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
}
}

View File

@ -3,6 +3,7 @@
namespace App\Filament\Pages\Installer;
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\RedisStep;
@ -12,15 +13,17 @@ use App\Services\Users\UserCreationService;
use App\Traits\CheckMigrationsTrait;
use App\Traits\EnvironmentWriterTrait;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\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\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\SimplePage;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Exceptions\Halt;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
@ -32,56 +35,46 @@ class PanelInstaller extends SimplePage implements HasForms
{
use CheckMigrationsTrait;
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;
public $data = [];
protected static string $view = 'filament.pages.installer';
private User $user;
public function getMaxWidth(): MaxWidth|string
{
return MaxWidth::SevenExtraLarge;
}
public static function show(): bool
public static function isInstalled(): bool
{
if (User::count() <= 0) {
return true;
}
if (config('panel.client_features.installer.enabled')) {
return true;
}
return false;
// This defaults to true so existing panels count as "installed"
return env('APP_INSTALLED', true);
}
public function mount()
{
abort_unless(self::show(), 404);
abort_if(self::isInstalled(), 404);
$this->form->fill();
}
public function dehydrate(): void
{
Artisan::call('config:clear');
Artisan::call('cache:clear');
}
protected function getFormSchema(): array
{
return [
Wizard::make([
RequirementsStep::make(),
EnvironmentStep::make(),
DatabaseStep::make(),
RedisStep::make()
->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'),
AdminUserStep::make(),
EnvironmentStep::make($this),
DatabaseStep::make($this),
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'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
type="submit"
@ -100,61 +93,89 @@ class PanelInstaller extends SimplePage implements HasForms
return 'data';
}
protected function hasUnsavedDataChangesAlert(): bool
{
return true;
}
public function submit()
{
// Disable installer
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
// Login user
$this->user ??= User::all()->filter(fn ($user) => $user->isRootAdmin())->first();
auth()->guard()->login($this->user, true);
// Redirect to admin panel
return redirect(Filament::getPanel('admin')->getUrl());
}
public function writeToEnv(string $key): void
{
try {
$inputs = $this->form->getState();
// Write variables to .env file
$variables = array_get($inputs, 'env');
$variables = array_get($this->data, $key);
$this->writeToEnvironment($variables);
// Clear config cache
Artisan::call('config:clear');
// Run migrations
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
'--database' => $variables['DB_CONNECTION'],
]);
if (!$this->hasCompletedMigrations()) {
throw new Exception('Migrations didn\'t run successfully. Double check your database configuration.');
}
// Create first admin user
$userData = array_get($inputs, 'user');
$userData['root_admin'] = true;
$user = app(UserCreationService::class)->handle($userData);
// Install setup complete
$this->writeToEnvironment(['APP_INSTALLER' => 'false']);
$this->rememberData();
Notification::make()
->title('Successfully Installed')
->success()
->send();
auth()->loginUsingId($user->id);
return redirect('/admin');
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Installation Failed')
->title('Could not write to .env file')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while writing .env file');
}
Artisan::call('config:clear');
}
public function runMigrations(string $driver): void
{
try {
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
'--database' => $driver,
]);
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Migrations failed')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while running migrations');
}
if (!$this->hasCompletedMigrations()) {
Notification::make()
->title('Migrations failed')
->danger()
->persistent()
->send();
throw new Halt('Migrations failed');
}
}
public function createAdminUser(): void
{
try {
$userData = array_get($this->data, 'user');
$userData['root_admin'] = true;
$this->user = app(UserCreationService::class)->handle($userData);
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Could not create admin user')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while creating admin user');
}
}
}

View File

@ -2,12 +2,13 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
class AdminUserStep
{
public static function make(): Step
public static function make(PanelInstaller $installer): Step
{
return Step::make('user')
->label('Admin User')
@ -26,6 +27,7 @@ class AdminUserStep
->required()
->password()
->revealable(),
]);
])
->afterValidation(fn () => $installer->createAdminUser());
}
}

View 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!'),
]);
}
}

View File

@ -2,37 +2,38 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
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\DB;
use PDOException;
class DatabaseStep
{
public static function make(): Step
public static function make(PanelInstaller $installer): Step
{
return Step::make('database')
->label('Database')
->columns()
->schema([
TextInput::make('env.DB_DATABASE')
->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
TextInput::make('env_database.DB_DATABASE')
->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.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(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
TextInput::make('env.DB_HOST')
->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')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your database. Make sure it is reachable.')
->required()
->default(env('DB_HOST', '127.0.0.1'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_PORT')
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_PORT')
->label('Database Port')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your database.')
@ -41,52 +42,68 @@ class DatabaseStep
->minValue(1)
->maxValue(65535)
->default(env('DB_PORT', 3306))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_USERNAME')
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_USERNAME')
->label('Database Username')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your database user.')
->required()
->default(env('DB_USERNAME', 'pelican'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_PASSWORD')
->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()
->default(env('DB_PASSWORD'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
->hidden(fn (Get $get) => $get('env_general.DB_CONNECTION') === 'sqlite'),
])
->afterValidation(function (Get $get) {
$driver = $get('env.DB_CONNECTION');
if ($driver !== 'sqlite') {
try {
config()->set('database.connections._panel_install_test', [
'driver' => $driver,
'host' => $get('env.DB_HOST'),
'port' => $get('env.DB_PORT'),
'database' => $get('env.DB_DATABASE'),
'username' => $get('env.DB_USERNAME'),
'password' => $get('env.DB_PASSWORD'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
->afterValidation(function (Get $get) use ($installer) {
$driver = $get('env_general.DB_CONNECTION');
DB::connection('_panel_install_test')->getPdo();
} catch (PDOException $exception) {
Notification::make()
->title('Database connection failed')
->body($exception->getMessage())
->danger()
->send();
DB::disconnect('_panel_install_test');
throw new Halt('Database connection failed');
}
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);
});
}
private static function testConnection(string $driver, $host, $port, $database, $username, $password): bool
{
if ($driver === 'sqlite') {
return true;
}
try {
config()->set('database.connections._panel_install_test', [
'driver' => $driver,
'host' => $host,
'port' => $port,
'database' => $database,
'username' => $username,
'password' => $password,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
DB::connection('_panel_install_test')->getPdo();
} catch (Exception $exception) {
DB::disconnect('_panel_install_test');
Notification::make()
->title('Database connection failed')
->body($exception->getMessage())
->danger()
->send();
return false;
}
return true;
}
}

View File

@ -2,14 +2,17 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use App\Traits\EnvironmentWriterTrait;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
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',
@ -17,14 +20,14 @@ class EnvironmentStep
public const SESSION_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
'database' => 'Database',
'cookie' => 'Cookie',
'redis' => 'Redis',
];
public const QUEUE_DRIVERS = [
'sync' => 'Sync',
'database' => 'Database',
'sync' => 'Sync',
'redis' => 'Redis',
];
@ -34,30 +37,30 @@ class EnvironmentStep
'mysql' => 'MySQL',
];
public static function make(): Step
public static function make(PanelInstaller $installer): Step
{
return Step::make('environment')
->label('Environment')
->columns()
->schema([
TextInput::make('env.APP_NAME')
TextInput::make('env_general.APP_NAME')
->label('App Name')
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the Name of your Panel.')
->required()
->default(config('app.name')),
TextInput::make('env.APP_URL')
TextInput::make('env_general.APP_URL')
->label('App URL')
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the URL you access your Panel from.')
->required()
->default(config('app.url'))
->default(url(''))
->live()
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))),
Toggle::make('env.SESSION_SECURE_COOKIE')
->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(env('SESSION_SECURE_COOKIE')),
ToggleButtons::make('env.CACHE_STORE')
->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".')
@ -65,7 +68,7 @@ class EnvironmentStep
->inline()
->options(self::CACHE_DRIVERS)
->default(config('cache.default', 'file')),
ToggleButtons::make('env.SESSION_DRIVER')
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".')
@ -73,15 +76,15 @@ class EnvironmentStep
->inline()
->options(self::SESSION_DRIVERS)
->default(config('session.driver', 'file')),
ToggleButtons::make('env.QUEUE_CONNECTION')
ToggleButtons::make('env_general.QUEUE_CONNECTION')
->label('Queue Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Sync" or "Database".')
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->required()
->inline()
->options(self::QUEUE_DRIVERS)
->default(config('queue.default', 'database')),
ToggleButtons::make('env.DB_CONNECTION')
ToggleButtons::make('env_general.DB_CONNECTION')
->label('Database Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
@ -89,6 +92,7 @@ class EnvironmentStep
->inline()
->options(self::DATABASE_DRIVERS)
->default(config('database.default', 'sqlite')),
]);
])
->afterValidation(fn () => $installer->writeToEnv('env_general'));
}
}

View File

@ -2,6 +2,8 @@
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;
@ -12,30 +14,32 @@ use Illuminate\Support\Facades\Redis;
class RedisStep
{
public static function make(): Step
use EnvironmentWriterTrait;
public static function make(PanelInstaller $installer): Step
{
return Step::make('redis')
->label('Redis')
->columns()
->schema([
TextInput::make('env.REDIS_HOST')
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_PORT')
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_USERNAME')
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_PASSWORD')
TextInput::make('env_redis.REDIS_PASSWORD')
->label('Redis Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.')
@ -43,25 +47,36 @@ class RedisStep
->revealable()
->default(config('database.redis.default.password')),
])
->afterValidation(function (Get $get) {
try {
config()->set('database.redis._panel_install_test', [
'host' => $get('env.REDIS_HOST'),
'username' => $get('env.REDIS_USERNAME'),
'password' => $get('env.REDIS_PASSWORD'),
'port' => $get('env.REDIS_PORT'),
]);
Redis::connection('_panel_install_test')->command('ping');
} catch (Exception $exception) {
Notification::make()
->title('Redis connection failed')
->body($exception->getMessage())
->danger()
->send();
->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($host, $port, $username, $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;
}
}

View File

@ -10,18 +10,20 @@ use Filament\Support\Exceptions\Halt;
class RequirementsStep
{
public const MIN_PHP_VERSION = '8.2';
public static function make(): Step
{
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0;
$correctPhpVersion = version_compare(PHP_VERSION, self::MIN_PHP_VERSION) >= 0;
$fields = [
Section::make('PHP Version')
->description('8.2 or newer')
->description(self::MIN_PHP_VERSION . ' or newer')
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
->iconColor($correctPhpVersion ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'),
->content('Your PHP Version is ' . PHP_VERSION . '.'),
]),
];
@ -80,7 +82,7 @@ class RequirementsStep
->danger()
->send();
throw new Halt();
throw new Halt('Some requirements are missing');
}
});
}

View File

@ -9,7 +9,6 @@ use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use App\Facades\Activity;
use Illuminate\View\View;
class LoginController extends AbstractLoginController
{
@ -20,7 +19,7 @@ class LoginController extends AbstractLoginController
*/
public function index()
{
if (PanelInstaller::show()) {
if (!PanelInstaller::isInstalled()) {
return redirect('/installer');
}

View File

@ -93,10 +93,6 @@ return [
'range_start' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_START'),
'range_end' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_END'),
],
'installer' => [
'enabled' => env('APP_INSTALLER', false),
],
],
/*

View File

@ -2,6 +2,4 @@
<x-filament-panels::form wire:submit="submit">
{{ $this->form }}
</x-filament-panels::form>
<x-filament-panels::page.unsaved-data-changes-alert />
</x-filament-panels::page.simple>