- 2.x - sites (wip)

- improved ssh error handling
- database soft deletes
This commit is contained in:
Saeed Vaziry
2024-10-04 21:34:07 +02:00
parent ecdba02bc9
commit d1f2add699
64 changed files with 1340 additions and 421 deletions

View File

@ -2,26 +2,18 @@
namespace App\Web\Pages\Servers\Console;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use App\Web\Pages\Servers\Page;
class Index extends Page
{
use PageHasServer;
protected ?string $live = '';
protected $listeners = [];
protected static ?string $slug = 'servers/{server}/console';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Console';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('update', static::getServerFromRoute()) ?? false;

View File

@ -61,15 +61,13 @@ public function form(Form $form): Form
$this->running = true;
$ssh = $this->server->ssh($this->data['user']);
$log = 'console-'.time();
defer(function () use ($ssh, $log) {
$ssh->exec(command: $this->data['command'], log: $log, stream: true, streamCallback: function ($output) {
$this->output .= $output;
$this->stream(
to: 'output',
content: $output,
);
});
})->name($log);
$ssh->exec(command: $this->data['command'], log: $log, stream: true, streamCallback: function ($output) {
$this->output .= $output;
$this->stream(
to: 'output',
content: $output,
);
});
}),
Action::make('stop')
->view('web.components.dynamic-widget', [

View File

@ -4,9 +4,7 @@
use App\Actions\CronJob\CreateCronJob;
use App\Models\CronJob;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use App\Web\Pages\Servers\Page;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
@ -16,18 +14,12 @@
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/cronjobs';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Cron Jobs';
protected $listeners = ['$refresh'];
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [CronJob::class, static::getServerFromRoute()]) ?? false;

View File

@ -4,12 +4,10 @@
use App\Actions\Database\CreateBackup;
use App\Models\Backup;
use App\Models\Server;
use App\Models\StorageProvider;
use App\Web\Components\Page;
use App\Web\Contracts\HasSecondSubNav;
use App\Web\Pages\Servers\Page;
use App\Web\Pages\Settings\StorageProviders\Actions\Create;
use App\Web\Traits\PageHasServer;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
@ -19,17 +17,12 @@
class Backups extends Page implements HasSecondSubNav
{
use PageHasServer;
use Traits\Navigation;
protected static ?string $slug = 'servers/{server}/databases/backups';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Backups';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Backup::class, static::getServerFromRoute()]) ?? false;

View File

@ -4,11 +4,8 @@
use App\Actions\Database\CreateDatabase;
use App\Models\Database;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Contracts\HasSecondSubNav;
use App\Web\Traits\PageHasServer;
use Exception;
use App\Web\Pages\Servers\Page;
use Filament\Actions\Action;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\TextInput;
@ -17,17 +14,12 @@
class Index extends Page implements HasSecondSubNav
{
use PageHasServer;
use Traits\Navigation;
protected static ?string $slug = 'servers/{server}/databases';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Databases';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Database::class, static::getServerFromRoute()]) ?? false;
@ -67,7 +59,7 @@ protected function getHeaderActions(): array
])
->modalSubmitActionLabel('Create')
->action(function (array $data) {
try {
run_action($this, function () use ($data) {
app(CreateDatabase::class)->create($this->server, $data);
$this->dispatch('$refresh');
@ -76,14 +68,7 @@ protected function getHeaderActions(): array
->success()
->title('Database Created!')
->send();
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
});
}),
];
}

View File

@ -5,10 +5,8 @@
use App\Actions\Database\CreateDatabase;
use App\Actions\Database\CreateDatabaseUser;
use App\Models\DatabaseUser;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Contracts\HasSecondSubNav;
use App\Web\Traits\PageHasServer;
use App\Web\Pages\Servers\Page;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Checkbox;
@ -18,17 +16,12 @@
class Users extends Page implements HasSecondSubNav
{
use PageHasServer;
use Traits\Navigation;
protected static ?string $slug = 'servers/{server}/databases/users';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Database Users';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [DatabaseUser::class, static::getServerFromRoute()]) ?? false;

View File

@ -5,7 +5,7 @@
use App\Actions\Database\RestoreBackup;
use App\Models\Backup;
use App\Models\BackupFile;
use Exception;
use App\Models\Database;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
@ -63,7 +63,7 @@ public function getTable(): Table
->icon('heroicon-o-arrow-path')
->modalHeading('Restore Backup')
->tooltip('Restore Backup')
->authorize(fn (BackupFile $record) => auth()->user()->can('update', $record->backup->database))
->authorize(fn (BackupFile $record) => auth()->user()->can('update', $record->backup))
->form([
Select::make('database')
->label('Restore to')
@ -73,23 +73,23 @@ public function getTable(): Table
])
->modalWidth(MaxWidth::Large)
->action(function (BackupFile $record, array $data) {
try {
run_action($this, function () use ($record, $data) {
$this->validate();
/** @var Database $database */
$database = Database::query()->findOrFail($data['database']);
$this->authorize('update', $database);
app(RestoreBackup::class)->restore($record, $data);
Notification::make()
->success()
->title('Backup is being restored')
->send();
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
$this->dispatch('$refresh');
});
}),
Action::make('delete')
->hiddenLabel()
@ -100,18 +100,10 @@ public function getTable(): Table
->authorize(fn (BackupFile $record) => auth()->user()->can('delete', $record))
->requiresConfirmation()
->action(function (BackupFile $record) {
try {
run_action($this, function () use ($record) {
$record->delete();
} catch (Exception $e) {
Notification::make()
->danger()
->title($e->getMessage())
->send();
throw $e;
}
$this->dispatch('$refresh');
$this->dispatch('$refresh');
});
}),
]);
}

View File

@ -4,9 +4,7 @@
use App\Actions\FirewallRule\CreateRule;
use App\Models\FirewallRule;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use App\Web\Pages\Servers\Page;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
@ -16,18 +14,12 @@
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/firewall';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Firewall';
protected $listeners = ['$refresh'];
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [FirewallRule::class, static::getServerFromRoute()]) ?? false;

View File

@ -2,24 +2,16 @@
namespace App\Web\Pages\Servers\Logs;
use App\Models\Server;
use App\Models\ServerLog;
use App\Web\Components\Page;
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
use App\Web\Traits\PageHasServer;
use App\Web\Pages\Servers\Page;
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/logs';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Logs';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [ServerLog::class, static::getServerFromRoute()]) ?? false;

View File

@ -4,9 +4,13 @@
use App\Models\Server;
use App\Models\ServerLog;
use App\Models\Site;
use Exception;
use Filament\Forms\Components\DatePicker;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
@ -18,13 +22,21 @@ class LogsList extends Widget
{
public Server $server;
public ?Site $site = null;
public ?string $label = '';
protected function getTableQuery(): Builder
{
return ServerLog::query()->where('server_id', $this->server->id);
return ServerLog::query()
->where('server_id', $this->server->id)
->where(function (Builder $query) {
if ($this->site) {
$query->where('site_id', $this->site->id);
}
});
}
protected static ?string $heading = '';
protected function getTableColumns(): array
{
return [
@ -68,6 +80,7 @@ public function getTable(): Table
);
}),
])
->heading($this->label)
->actions([
Action::make('view')
->hiddenLabel()
@ -90,6 +103,19 @@ public function getTable(): Table
->icon('heroicon-o-archive-box-arrow-down')
->authorize(fn ($record) => auth()->user()->can('view', $record))
->action(fn (ServerLog $record) => $record->download()),
]);
DeleteAction::make()
->hiddenLabel()
->tooltip('Delete')
->icon('heroicon-o-trash')
->color('danger')
->authorize(fn ($record) => auth()->user()->can('delete', $record)),
])
->bulkActions(
BulkActionGroup::make([
DeleteBulkAction::make()
->requiresConfirmation()
->authorize(auth()->user()->can('deleteMany', [ServerLog::class, $this->server])),
])
);
}
}

View File

@ -3,22 +3,14 @@
namespace App\Web\Pages\Servers\Metrics;
use App\Models\Metric;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use App\Web\Pages\Servers\Page;
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/metrics';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Metrics';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Metric::class, static::getServerFromRoute()]) ?? false;

View File

@ -3,27 +3,19 @@
namespace App\Web\Pages\Servers\PHP;
use App\Actions\PHP\InstallNewPHP;
use App\Models\Server;
use App\Models\Service;
use App\Web\Components\Page;
use App\Web\Pages\Servers\Page;
use App\Web\Pages\Servers\PHP\Widgets\PHPList;
use App\Web\Traits\PageHasServer;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Support\Enums\IconPosition;
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/php';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'PHP';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Service::class, static::getServerFromRoute()]) ?? false;

View File

@ -0,0 +1,153 @@
<?php
namespace App\Web\Pages\Servers;
use App\Models\Server;
use App\Web\Components\Page as BasePage;
use App\Web\Pages\Servers\Console\Index as ConsoleIndex;
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
use App\Web\Pages\Servers\Databases\Index as DatabasesIndex;
use App\Web\Pages\Servers\Firewall\Index as FirewallIndex;
use App\Web\Pages\Servers\Logs\Index as LogsIndex;
use App\Web\Pages\Servers\Metrics\Index as MetricsIndex;
use App\Web\Pages\Servers\PHP\Index as PHPIndex;
use App\Web\Pages\Servers\Services\Index as ServicesIndex;
use App\Web\Pages\Servers\Settings as ServerSettings;
use App\Web\Pages\Servers\Sites\Index as SitesIndex;
use App\Web\Pages\Servers\SSHKeys\Index as SshKeysIndex;
use App\Web\Pages\Servers\View as ServerView;
use App\Web\Pages\Servers\Widgets\ServerSummary;
use Filament\Navigation\NavigationItem;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Route;
abstract class Page extends BasePage
{
public Server $server;
protected static bool $shouldRegisterNavigation = false;
public function getSubNavigation(): array
{
$items = [];
if (ServerView::canAccess()) {
$items[] = NavigationItem::make(ServerView::getNavigationLabel())
->icon('heroicon-o-chart-pie')
->isActiveWhen(fn () => request()->routeIs(ServerView::getRouteName()))
->url(ServerView::getUrl(parameters: ['server' => $this->server]));
}
if (SitesIndex::canAccess()) {
$items[] = NavigationItem::make(SitesIndex::getNavigationLabel())
->icon('heroicon-o-cursor-arrow-ripple')
->isActiveWhen(fn () => request()->routeIs(SitesIndex::getRouteName().'*'))
->url(SitesIndex::getUrl(parameters: ['server' => $this->server]));
}
if (DatabasesIndex::canAccess()) {
$items[] = NavigationItem::make(DatabasesIndex::getNavigationLabel())
->icon('heroicon-o-circle-stack')
->isActiveWhen(fn () => request()->routeIs(DatabasesIndex::getRouteName().'*'))
->url(DatabasesIndex::getUrl(parameters: ['server' => $this->server]));
}
if (PHPIndex::canAccess()) {
$items[] = NavigationItem::make(PHPIndex::getNavigationLabel())
->icon('heroicon-o-code-bracket')
->isActiveWhen(fn () => request()->routeIs(PHPIndex::getRouteName().'*'))
->url(PHPIndex::getUrl(parameters: ['server' => $this->server]));
}
if (FirewallIndex::canAccess()) {
$items[] = NavigationItem::make(FirewallIndex::getNavigationLabel())
->icon('heroicon-o-fire')
->isActiveWhen(fn () => request()->routeIs(FirewallIndex::getRouteName().'*'))
->url(FirewallIndex::getUrl(parameters: ['server' => $this->server]));
}
if (CronJobsIndex::canAccess()) {
$items[] = NavigationItem::make(CronJobsIndex::getNavigationLabel())
->icon('heroicon-o-clock')
->isActiveWhen(fn () => request()->routeIs(CronJobsIndex::getRouteName().'*'))
->url(CronJobsIndex::getUrl(parameters: ['server' => $this->server]));
}
if (SshKeysIndex::canAccess()) {
$items[] = NavigationItem::make(SshKeysIndex::getNavigationLabel())
->icon('heroicon-o-key')
->isActiveWhen(fn () => request()->routeIs(SshKeysIndex::getRouteName().'*'))
->url(SshKeysIndex::getUrl(parameters: ['server' => $this->server]));
}
if (ServicesIndex::canAccess()) {
$items[] = NavigationItem::make(ServicesIndex::getNavigationLabel())
->icon('heroicon-o-cog-6-tooth')
->isActiveWhen(fn () => request()->routeIs(ServicesIndex::getRouteName().'*'))
->url(ServicesIndex::getUrl(parameters: ['server' => $this->server]));
}
if (MetricsIndex::canAccess()) {
$items[] = NavigationItem::make(MetricsIndex::getNavigationLabel())
->icon('heroicon-o-chart-bar')
->isActiveWhen(fn () => request()->routeIs(MetricsIndex::getRouteName().'*'))
->url(MetricsIndex::getUrl(parameters: ['server' => $this->server]));
}
if (ConsoleIndex::canAccess()) {
$items[] = NavigationItem::make(ConsoleIndex::getNavigationLabel())
->icon('heroicon-o-command-line')
->isActiveWhen(fn () => request()->routeIs(ConsoleIndex::getRouteName().'*'))
->url(ConsoleIndex::getUrl(parameters: ['server' => $this->server]));
}
if (LogsIndex::canAccess()) {
$items[] = NavigationItem::make(LogsIndex::getNavigationLabel())
->icon('heroicon-o-square-3-stack-3d')
->isActiveWhen(fn () => request()->routeIs(LogsIndex::getRouteName().'*'))
->url(LogsIndex::getUrl(parameters: ['server' => $this->server]));
}
if (ServerSettings::canAccess()) {
$items[] = NavigationItem::make(ServerSettings::getNavigationLabel())
->icon('heroicon-o-wrench-screwdriver')
->isActiveWhen(fn () => request()->routeIs(ServerSettings::getRouteName().'*'))
->url(ServerSettings::getUrl(parameters: ['server' => $this->server]));
}
return $items;
}
protected function getHeaderWidgets(): array
{
return [
ServerSummary::make([
'server' => $this->server,
]),
];
}
protected static function getServerFromRoute(): ?Server
{
$server = request()->route('server');
if (! $server) {
$server = Route::getRoutes()->match(Request::create(url()->previous()))->parameter('server');
}
if ($server instanceof Server) {
return $server;
}
if ($server) {
return Server::query()->find($server);
}
return null;
}
public function getHeaderWidgetsColumns(): int|string|array
{
return 1;
}
}

View File

@ -4,10 +4,8 @@
use App\Actions\SshKey\CreateSshKey;
use App\Actions\SshKey\DeployKeyToServer;
use App\Models\Server;
use App\Models\SshKey;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use App\Web\Pages\Servers\Page;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
@ -18,16 +16,10 @@
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/ssh-keys';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'SSH Keys';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAnyServer', [SshKey::class, static::getServerFromRoute()]) ?? false;

View File

@ -3,10 +3,8 @@
namespace App\Web\Pages\Servers\Services;
use App\Actions\Service\Install;
use App\Models\Server;
use App\Models\Service;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use App\Web\Pages\Servers\Page;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
@ -15,16 +13,10 @@
class Index extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/services';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Services';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Service::class, static::getServerFromRoute()]) ?? false;

View File

@ -4,18 +4,14 @@
use App\Actions\Server\RebootServer;
use App\Models\Server;
use App\Web\Components\Page;
use App\Web\Pages\Servers\Widgets\ServerDetails;
use App\Web\Pages\Servers\Widgets\UpdateServerInfo;
use App\Web\Traits\PageHasServer;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
class Settings extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/settings';
protected static bool $shouldRegisterNavigation = false;
@ -24,8 +20,6 @@ class Settings extends Page
protected $listeners = ['$refresh'];
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('update', static::getServerFromRoute()) ?? false;

View File

@ -2,24 +2,28 @@
namespace App\Web\Pages\Servers\Sites;
use App\Models\Server;
use App\Actions\Site\CreateSite;
use App\Enums\SiteType;
use App\Models\Site;
use App\Web\Components\Page;
use App\Web\Traits\PageHasServer;
use Filament\Actions\CreateAction;
use App\Models\SourceControl;
use App\Web\Pages\Settings\SourceControls\Actions\Create;
use Filament\Actions\Action;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Support\Enums\MaxWidth;
use Throwable;
class Index extends Page
class Index extends \App\Web\Pages\Servers\Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}/sites';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Sites';
public Server $server;
public static function canAccess(): bool
{
return auth()->user()?->can('viewAny', [Site::class, static::getServerFromRoute()]) ?? false;
@ -35,11 +39,107 @@ public function getWidgets(): array
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->authorize(fn () => auth()->user()?->can('create', [Site::class, $this->server]))
->createAnother(false)
Action::make('read-the-docs')
->label('Read the Docs')
->icon('heroicon-o-document-text')
->color('gray')
->url('https://vitodeploy.com/sites/create-site.html')
->openUrlInNewTab(),
Action::make('create')
->label('Create a Site')
->icon('heroicon-o-plus'),
->icon('heroicon-o-plus')
->authorize(fn () => auth()->user()?->can('create', [Site::class, $this->server]))
->modalWidth(MaxWidth::FiveExtraLarge)
->slideOver()
->form([
Select::make('type')
->options(
collect(config('core.site_types'))->mapWithKeys(fn ($type) => [$type => $type])
)
->reactive()
->afterStateUpdated(function (?string $state, Set $set) {
if ($state === SiteType::LARAVEL) {
$set('web_directory', 'public');
} else {
$set('web_directory', '');
}
})
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['type']),
TextInput::make('domain')
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['domain']),
TagsInput::make('aliases')
->splitKeys(['Enter', 'Tab', ' ', ','])
->placeholder('Type and press enter to add an alias')
->nestedRecursiveRules(CreateSite::rules($this->server, [])['aliases.*']),
Select::make('php_version')
->label('PHP Version')
->options(collect($this->server->installedPHPVersions())->mapWithKeys(fn ($version) => [$version => $version]))
->visible(fn (Get $get) => isset(CreateSite::rules($this->server, $get())['php_version']))
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['php_version']),
TextInput::make('web_directory')
->placeholder('For / leave empty')
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['web_directory'])
->visible(fn (Get $get) => isset(CreateSite::rules($this->server, $get())['web_directory']))
->helperText(
sprintf(
'The relative path of your website from /home/%s/your-domain/',
$this->server->ssh_user
)
),
Select::make('source_control')
->label('Source Control')
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['source_control'])
->options(
SourceControl::getByProjectId(auth()->user()->current_project_id)
->pluck('profile', 'id')
)
->suffixAction(
\Filament\Forms\Components\Actions\Action::make('connect')
->form(Create::form())
->modalHeading('Connect to a source control')
->modalSubmitActionLabel('Connect')
->icon('heroicon-o-wifi')
->tooltip('Connect to a source control')
->modalWidth(MaxWidth::Large)
->authorize(fn () => auth()->user()->can('create', SourceControl::class))
->action(fn (array $data) => Create::action($data))
)
->placeholder('Select source control')
->live()
->visible(fn (Get $get) => isset(CreateSite::rules($this->server, $get())['source_control'])),
TextInput::make('repository')
->placeholder('organization/repository')
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['repository'])
->visible(fn (Get $get) => isset(CreateSite::rules($this->server, $get())['repository'])),
TextInput::make('branch')
->placeholder('main')
->rules(fn (Get $get) => CreateSite::rules($this->server, $get())['branch'])
->visible(fn (Get $get) => isset(CreateSite::rules($this->server, $get())['branch'])),
Checkbox::make('composer')
->label('Run `composer install --no-dev`')
->default(false)
->visible(fn (Get $get) => isset(CreateSite::rules($this->server, $get())['composer'])),
])
->action(function (array $data) {
$this->authorize('create', [Site::class, $this->server]);
$this->validate();
try {
$site = app(CreateSite::class)->create($this->server, $data);
$this->redirect(\App\Web\Pages\Servers\Sites\View::getUrl([
'server' => $this->server,
'site' => $site,
]));
} catch (Throwable $e) {
Notification::make()
->title($e->getMessage())
->danger()
->send();
}
})
->modalSubmitActionLabel('Create Site'),
];
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace App\Web\Pages\Servers\Sites;
use App\Models\Site;
use App\Web\Contracts\HasSecondSubNav;
use App\Web\Pages\Servers\Page as BasePage;
use App\Web\Pages\Servers\Sites\Widgets\SiteSummary;
use Filament\Navigation\NavigationGroup;
use Filament\Navigation\NavigationItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
abstract class Page extends BasePage implements HasSecondSubNav
{
public Site $site;
public function getSecondSubNavigation(): array
{
$items = [];
if (View::canAccess()) {
$items[] = NavigationItem::make(View::getNavigationLabel())
->icon('heroicon-o-globe-alt')
->isActiveWhen(fn () => request()->routeIs(View::getRouteName()))
->url(View::getUrl(parameters: [
'server' => $this->server,
'site' => $this->site,
]));
}
if (Pages\SSL\Index::canAccess()) {
$items[] = NavigationItem::make(Pages\SSL\Index::getNavigationLabel())
->icon('heroicon-o-lock-closed')
->isActiveWhen(fn () => request()->routeIs(Pages\SSL\Index::getRouteName()))
->url(Pages\SSL\Index::getUrl(parameters: [
'server' => $this->server,
'site' => $this->site,
]));
}
if (Pages\Queues\Index::canAccess()) {
$items[] = NavigationItem::make(Pages\Queues\Index::getNavigationLabel())
->icon('heroicon-o-queue-list')
->isActiveWhen(fn () => request()->routeIs(Pages\Queues\Index::getRouteName()))
->url(Pages\Queues\Index::getUrl(parameters: [
'server' => $this->server,
'site' => $this->site,
]));
}
return [
NavigationGroup::make()
->items($items),
];
}
protected function getHeaderWidgets(): array
{
return array_merge(parent::getHeaderWidgets(), [
SiteSummary::make(['site' => $this->site]),
]);
}
protected static function getSiteFromRoute(): ?Site
{
$site = request()->route('site');
if (! $site) {
$site = Route::getRoutes()->match(Request::create(url()->previous()))->parameter('site');
}
if ($site instanceof Site) {
return $site;
}
if ($site) {
return Site::query()->find($site);
}
return null;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Web\Pages\Servers\Sites\Pages\Queues;
use App\Web\Pages\Servers\Sites\Page;
class Index extends Page
{
protected static ?string $slug = 'servers/{server}/sites/{site}/queues';
protected static ?string $title = 'Queues';
public static function canAccess(): bool
{
return true;
}
public function getWidgets(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Web\Pages\Servers\Sites\Pages\SSL;
use App\Web\Pages\Servers\Sites\Page;
class Index extends Page
{
protected static ?string $slug = 'servers/{server}/sites/{site}/ssl';
protected static ?string $title = 'SSL';
public static function canAccess(): bool
{
return true;
}
public function getWidgets(): array
{
return [];
}
}

View File

@ -0,0 +1,198 @@
<?php
namespace App\Web\Pages\Servers\Sites;
use App\Actions\Site\Deploy;
use App\Actions\Site\UpdateBranch;
use App\Actions\Site\UpdateDeploymentScript;
use App\Actions\Site\UpdateEnv;
use App\Enums\SiteFeature;
use App\Models\ServerLog;
use App\Web\Fields\CodeEditorField;
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Support\Enums\IconPosition;
use Filament\Support\Enums\MaxWidth;
use Livewire\Attributes\On;
class View extends Page
{
protected static ?string $slug = 'servers/{server}/sites/{site}';
protected static ?string $title = 'Application';
public string $previousStatus;
public function mount(): void
{
$this->previousStatus = $this->site->status;
}
public static function canAccess(): bool
{
return auth()->user()?->can('view', [static::getSiteFromRoute(), static::getServerFromRoute()]) ?? false;
}
#[On('$refresh')]
public function refresh(): void
{
$currentStatus = $this->site->refresh()->status;
if ($this->previousStatus !== $currentStatus) {
$this->redirect(static::getUrl(parameters: [
'server' => $this->server,
'site' => $this->site,
]));
}
$this->previousStatus = $currentStatus;
}
public function getWidgets(): array
{
$widgets = [];
if ($this->site->isInstalling()) {
$widgets[] = [Widgets\Installing::class, ['site' => $this->site]];
if (auth()->user()->can('viewAny', [ServerLog::class, $this->server])) {
$widgets[] = [
LogsList::class, [
'server' => $this->server,
'site' => $this->site,
'label' => 'Logs',
],
];
}
}
return $widgets;
}
public function getHeaderActions(): array
{
$actions = [];
$actionsGroup = [];
if (in_array(SiteFeature::DEPLOYMENT, $this->site->type()->supportedFeatures())) {
$actions[] = $this->deployAction();
$actionsGroup[] = $this->deploymentScriptAction();
}
if (in_array(SiteFeature::ENV, $this->site->type()->supportedFeatures())) {
$actionsGroup[] = $this->dotEnvAction();
}
$actionsGroup[] = $this->branchAction();
$actions[] = ActionGroup::make($actionsGroup)
->button()
->color('gray')
->icon('heroicon-o-chevron-up-down')
->iconPosition(IconPosition::After)
->dropdownPlacement('bottom-end');
return $actions;
}
public function getSecondSubNavigation(): array
{
if ($this->site->isInstalling()) {
return [];
}
return parent::getSecondSubNavigation();
}
private function deployAction(): Action
{
return Action::make('deploy')
->icon('heroicon-o-rocket-launch')
->action(function () {
run_action($this, function () {
app(Deploy::class)->run($this->site);
Notification::make()
->success()
->title('Deployment started!')
->send();
});
});
}
private function deploymentScriptAction(): Action
{
return Action::make('deployment-script')
->label('Deployment Script')
->modalSubmitActionLabel('Save')
->modalHeading('Update Deployment Script')
->form([
CodeEditorField::make('script')
->default($this->site->deploymentScript?->content)
->rules(UpdateDeploymentScript::rules()['script']),
])
->action(function (array $data) {
run_action($this, function () use ($data) {
app(UpdateDeploymentScript::class)->update($this->site, $data);
Notification::make()
->success()
->title('Deployment script updated!')
->send();
});
});
}
private function dotEnvAction(): Action
{
return Action::make('dot-env')
->label('Update .env')
->modalSubmitActionLabel('Save')
->modalHeading('Update .env file')
->form([
CodeEditorField::make('env')
->formatStateUsing(function () {
return $this->site->getEnv();
})
->rules([
'env' => 'required',
]),
])
->action(function (array $data) {
run_action($this, function () use ($data) {
app(UpdateEnv::class)->update($this->site, $data);
Notification::make()
->success()
->title('.env updated!')
->send();
});
});
}
private function branchAction(): Action
{
return Action::make('branch')
->label('Branch')
->modalSubmitActionLabel('Save')
->modalHeading('Change branch')
->modalWidth(MaxWidth::Medium)
->form([
TextInput::make('branch')
->default($this->site->branch)
->rules(UpdateBranch::rules()['branch']),
])
->action(function (array $data) {
run_action($this, function () use ($data) {
app(UpdateBranch::class)->update($this->site, $data);
Notification::make()
->success()
->title('Branch updated!')
->send();
});
});
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Web\Pages\Servers\Sites\Widgets;
use App\Models\Site;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Components\Section;
use Filament\Infolists\Components\ViewEntry;
use Filament\Infolists\Concerns\InteractsWithInfolists;
use Filament\Infolists\Contracts\HasInfolists;
use Filament\Infolists\Infolist;
use Filament\Widgets\Widget;
use Illuminate\View\ComponentAttributeBag;
class Installing extends Widget implements HasForms, HasInfolists
{
use InteractsWithForms;
use InteractsWithInfolists;
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
protected static string $view = 'web.components.infolist';
public Site $site;
public function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make()
->heading('Installing Site')
->icon(function () {
if ($this->site->isInstallationFailed()) {
return 'heroicon-o-x-circle';
}
return view('filament::components.loading-indicator')
->with('attributes', new ComponentAttributeBag([
'class' => 'mr-2 size-[24px] text-primary-400',
]));
})
->iconColor($this->site->isInstallationFailed() ? 'danger' : 'primary')
->schema([
ViewEntry::make('progress')
->hiddenLabel()
->view('components.progress-bar')
->viewData([
'value' => $this->site->progress,
]),
]),
])
->record($this->site->refresh());
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Web\Pages\Servers\Sites\Widgets;
use App\Models\Site;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Components\Fieldset;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Concerns\InteractsWithInfolists;
use Filament\Infolists\Contracts\HasInfolists;
use Filament\Infolists\Infolist;
use Filament\Support\Enums\IconPosition;
use Filament\Widgets\Widget;
class SiteSummary extends Widget implements HasForms, HasInfolists
{
use InteractsWithForms;
use InteractsWithInfolists;
protected $listeners = ['$refresh'];
protected static bool $isLazy = false;
protected static string $view = 'web.components.infolist';
public Site $site;
public function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Fieldset::make('info')
->label('Site Summary')
->schema([
TextEntry::make('domain')
->icon('heroicon-o-clipboard-document')
->iconPosition(IconPosition::After)
->copyable(),
TextEntry::make('path')
->icon('heroicon-o-clipboard-document')
->iconPosition(IconPosition::After)
->copyable(),
TextEntry::make('status')
->label('Status')
->badge()
->color(static function ($state): string {
return Site::$statusColors[$state];
}),
])
->columns(3),
])
->record($this->site->refresh());
}
}

View File

@ -4,7 +4,7 @@
use App\Models\Server;
use App\Models\Site;
use App\Web\Pages\Servers\View;
use App\Web\Pages\Servers\Sites\View;
use Filament\Tables\Actions\Action;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@ -28,11 +28,12 @@ protected function getTableColumns(): array
TextColumn::make('id')
->searchable()
->sortable(),
TextColumn::make('server.name')
TextColumn::make('domain')
->searchable()
->sortable(),
TextColumn::make('domain')
->searchable(),
TextColumn::make('created_at')
->formatStateUsing(fn (Site $record) => $record->created_at_by_timezone)
->sortable(),
TextColumn::make('status')
->label('Status')
->badge()
@ -45,13 +46,13 @@ protected function getTableColumns(): array
public function getTable(): Table
{
return $this->table
// ->recordUrl(fn (Server $record) => View::getUrl(parameters: ['server' => $record]))
->recordUrl(fn (Site $record) => View::getUrl(parameters: ['server' => $this->server, 'site' => $record]))
->actions([
// Action::make('settings')
// ->label('Settings')
// ->icon('heroicon-o-cog-6-tooth')
// ->authorize(fn ($record) => auth()->user()->can('update', $record))
// ->url(fn (Server $record) => '/'),
Action::make('settings')
->label('Settings')
->icon('heroicon-o-cog-6-tooth')
->authorize(fn (Site $record) => auth()->user()->can('update', [$record, $this->server]))
->url(fn (Site $record) => '/'),
]);
}
}

View File

@ -2,27 +2,18 @@
namespace App\Web\Pages\Servers;
use App\Models\Server;
use App\Models\ServerLog;
use App\Web\Components\Page;
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
use App\Web\Pages\Servers\Widgets\Installing;
use App\Web\Pages\Servers\Widgets\ServerStats;
use App\Web\Traits\PageHasServer;
use Livewire\Attributes\On;
class View extends Page
{
use PageHasServer;
protected static ?string $slug = 'servers/{server}';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $title = 'Overview';
public Server $server;
public string $previousStatus;
public function mount(): void
@ -58,7 +49,12 @@ public function getWidgets(): array
}
if (auth()->user()->can('viewAny', [ServerLog::class, $this->server])) {
$widgets[] = [LogsList::class, ['server' => $this->server]];
$widgets[] = [
LogsList::class, [
'server' => $this->server,
'label' => 'Logs',
],
];
}
return $widgets;

View File

@ -73,6 +73,7 @@ public function infolist(Infolist $infolist): Infolist
Action::make('update-server')
->icon('heroicon-o-check-circle')
->tooltip('Update Now')
->requiresConfirmation()
->action(function (Server $record) {
app(Update::class)->update($record);