mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-02 14:36:17 +00:00
- 2.x - sites (wip)
- improved ssh error handling - database soft deletes
This commit is contained in:
@ -14,7 +14,6 @@
|
||||
use App\ValidationRules\DomainRule;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
@ -25,8 +24,6 @@ class CreateSite
|
||||
*/
|
||||
public function create(Server $server, array $input): Site
|
||||
{
|
||||
$this->validateInputs($server, $input);
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$site = new Site([
|
||||
@ -60,9 +57,6 @@ public function create(Server $server, array $input): Site
|
||||
]);
|
||||
}
|
||||
|
||||
// validate type
|
||||
$this->validateType($site, $input);
|
||||
|
||||
// set type data
|
||||
$site->type_data = $site->type()->data($input);
|
||||
|
||||
@ -101,13 +95,9 @@ public function create(Server $server, array $input): Site
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function rules(array $input): void
|
||||
public static function rules(Server $server, array $input): array
|
||||
{
|
||||
$rules = [
|
||||
'server_id' => [
|
||||
'required',
|
||||
'exists:servers,id',
|
||||
],
|
||||
'type' => [
|
||||
'required',
|
||||
Rule::in(config('core.site_types')),
|
||||
@ -124,16 +114,20 @@ public static function rules(array $input): void
|
||||
],
|
||||
];
|
||||
|
||||
Validator::make($input, $rules)->validate();
|
||||
return array_merge($rules, self::typeRules($server, $input));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateType(Site $site, array $input): void
|
||||
private static function typeRules(Server $server, array $input): array
|
||||
{
|
||||
$rules = $site->type()->createRules($input);
|
||||
if (! isset($input['type']) || ! in_array($input['type'], config('core.site_types'))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
Validator::make($input, $rules)->validate();
|
||||
$site = new Site([
|
||||
'server_id' => $server->id,
|
||||
'type' => $input['type']]
|
||||
);
|
||||
|
||||
return $site->type()->createRules($input);
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,15 @@
|
||||
namespace App\Actions\Site;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\SSH\Services\Webserver\Webserver;
|
||||
|
||||
class DeleteSite
|
||||
{
|
||||
public function delete(Site $site): void
|
||||
{
|
||||
$site->server->webserver()->handler()->deleteSite($site);
|
||||
/** @var Webserver $webserverHandler */
|
||||
$webserverHandler = $site->server->webserver()->handler();
|
||||
$webserverHandler->deleteSite($site);
|
||||
$site->delete();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
use App\Models\Site;
|
||||
use App\SSH\Git\Git;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class UpdateBranch
|
||||
@ -14,7 +13,6 @@ class UpdateBranch
|
||||
*/
|
||||
public function update(Site $site, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
$site->branch = $input['branch'];
|
||||
app(Git::class)->checkout($site);
|
||||
$site->save();
|
||||
@ -23,10 +21,10 @@ public function update(Site $site, array $input): void
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function validate(array $input): void
|
||||
public static function rules(): array
|
||||
{
|
||||
Validator::make($input, [
|
||||
return [
|
||||
'branch' => 'required',
|
||||
]);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
namespace App\Actions\Site;
|
||||
|
||||
use App\Models\Site;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class UpdateDeploymentScript
|
||||
@ -13,8 +12,6 @@ class UpdateDeploymentScript
|
||||
*/
|
||||
public function update(Site $site, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
$site->deploymentScript()->update([
|
||||
'content' => $input['script'],
|
||||
]);
|
||||
@ -23,10 +20,10 @@ public function update(Site $site, array $input): void
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function validate(array $input): void
|
||||
public static function rules(): array
|
||||
{
|
||||
Validator::make($input, [
|
||||
'script' => 'required',
|
||||
]);
|
||||
return [
|
||||
'script' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
use App\Models\SourceControl;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
@ -13,8 +12,6 @@ class ConnectSourceControl
|
||||
{
|
||||
public function connect(User $user, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
$sourceControl = new SourceControl([
|
||||
'provider' => $input['provider'],
|
||||
'profile' => $input['name'],
|
||||
@ -22,8 +19,6 @@ public function connect(User $user, array $input): void
|
||||
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
|
||||
]);
|
||||
|
||||
$this->validateProvider($sourceControl, $input);
|
||||
|
||||
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
|
||||
|
||||
if (! $sourceControl->provider()->connect()) {
|
||||
@ -36,28 +31,34 @@ public function connect(User $user, array $input): void
|
||||
$sourceControl->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validate(array $input): void
|
||||
public static function rules(array $input): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
'provider' => [
|
||||
'required',
|
||||
Rule::in(config('core.source_control_providers')),
|
||||
],
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
];
|
||||
Validator::make($input, $rules)->validate();
|
||||
|
||||
return array_merge($rules, static::providerRules($input));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateProvider(SourceControl $sourceControl, array $input): void
|
||||
private static function providerRules(array $input): array
|
||||
{
|
||||
Validator::make($input, $sourceControl->provider()->createRules($input))->validate();
|
||||
if (! isset($input['provider'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sourceControl = new SourceControl([
|
||||
'provider' => $input['provider'],
|
||||
]);
|
||||
|
||||
return $sourceControl->provider()->createRules($input);
|
||||
}
|
||||
}
|
||||
|
@ -4,21 +4,17 @@
|
||||
|
||||
use App\Models\SourceControl;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EditSourceControl
|
||||
{
|
||||
public function edit(SourceControl $sourceControl, User $user, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
$sourceControl->profile = $input['name'];
|
||||
$sourceControl->url = $input['url'] ?? null;
|
||||
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
|
||||
|
||||
$this->validateProvider($sourceControl, $input);
|
||||
|
||||
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
|
||||
|
||||
if (! $sourceControl->provider()->connect()) {
|
||||
@ -31,24 +27,34 @@ public function edit(SourceControl $sourceControl, User $user, array $input): vo
|
||||
$sourceControl->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validate(array $input): void
|
||||
public static function rules(array $input): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
'provider' => [
|
||||
'required',
|
||||
Rule::in(config('core.source_control_providers')),
|
||||
],
|
||||
];
|
||||
Validator::make($input, $rules)->validate();
|
||||
|
||||
return array_merge($rules, static::providerRules($input));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateProvider(SourceControl $sourceControl, array $input): void
|
||||
private static function providerRules(array $input): array
|
||||
{
|
||||
Validator::make($input, $sourceControl->provider()->createRules($input))->validate();
|
||||
if (! isset($input['provider'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sourceControl = new SourceControl([
|
||||
'provider' => $input['provider'],
|
||||
]);
|
||||
|
||||
return $sourceControl->provider()->createRules($input);
|
||||
}
|
||||
}
|
||||
|
@ -13,4 +13,21 @@ final class SiteType
|
||||
const WORDPRESS = 'wordpress';
|
||||
|
||||
const PHPMYADMIN = 'phpmyadmin';
|
||||
|
||||
public static function hasWebDirectory(): array
|
||||
{
|
||||
return [
|
||||
self::PHP,
|
||||
self::PHP_BLANK,
|
||||
self::LARAVEL,
|
||||
];
|
||||
}
|
||||
|
||||
public static function hasSourceControl(): array
|
||||
{
|
||||
return [
|
||||
self::PHP,
|
||||
self::LARAVEL,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,23 @@
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Models\ServerLog;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class SSHError extends Exception
|
||||
{
|
||||
//
|
||||
protected ?ServerLog $log;
|
||||
|
||||
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null, ?ServerLog $log = null)
|
||||
{
|
||||
$this->log = $log;
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getLog(): ?ServerLog
|
||||
{
|
||||
return $this->log;
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,7 @@ public function connect(bool $sftp = false): void
|
||||
*/
|
||||
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string
|
||||
{
|
||||
ds($command);
|
||||
if (! $this->log && $log) {
|
||||
$this->log = ServerLog::make($this->server, $log);
|
||||
if ($siteId) {
|
||||
@ -129,13 +130,19 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
|
||||
$this->log?->write($output);
|
||||
|
||||
if ($this->connection->getExitStatus() !== 0 || Str::contains($output, 'VITO_SSH_ERROR')) {
|
||||
throw new SSHCommandError('SSH command failed with an error', $this->connection->getExitStatus());
|
||||
throw new SSHCommandError(
|
||||
message: 'SSH command failed with an error',
|
||||
log: $this->log
|
||||
);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
throw new SSHCommandError($e->getMessage());
|
||||
throw new SSHCommandError(
|
||||
message: $e->getMessage(),
|
||||
log: $this->log
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Exceptions\SSHCommandError;
|
||||
use App\Exceptions\SSHConnectionError;
|
||||
use App\Facades\Toast;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class HandleSSHErrors
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$res = $next($request);
|
||||
// if ($res instanceof Response && $res->exception) {
|
||||
// if ($res->exception instanceof SSHConnectionError || $res->exception instanceof SSHCommandError) {
|
||||
// Toast::error($res->exception->getMessage());
|
||||
|
||||
// if ($request->hasHeader('HX-Request')) {
|
||||
// return htmx()->back();
|
||||
// }
|
||||
|
||||
// return back();
|
||||
// }
|
||||
// }
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
@ -70,7 +70,7 @@ public function storage(): BelongsTo
|
||||
|
||||
public function database(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Database::class);
|
||||
return $this->belongsTo(Database::class)->withTrashed();
|
||||
}
|
||||
|
||||
public function files(): HasMany
|
||||
|
@ -3,9 +3,11 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\DatabaseStatus;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @property int $server_id
|
||||
@ -13,10 +15,12 @@
|
||||
* @property string $status
|
||||
* @property Server $server
|
||||
* @property Backup[] $backups
|
||||
* @property Carbon $deleted_at
|
||||
*/
|
||||
class Database extends AbstractModel
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'server_id',
|
||||
@ -41,9 +45,6 @@ public static function boot(): void
|
||||
$user->save();
|
||||
}
|
||||
});
|
||||
$database->backups()->each(function (Backup $backup) {
|
||||
$backup->delete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -97,13 +97,17 @@ public function write($buf): void
|
||||
}
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
public function getContent($lines = null): ?string
|
||||
{
|
||||
if ($this->is_remote) {
|
||||
return $this->server->os()->tail($this->name, 150);
|
||||
return $this->server->os()->tail($this->name, $lines ?? 150);
|
||||
}
|
||||
|
||||
if (Storage::disk($this->disk)->exists($this->name)) {
|
||||
if ($lines) {
|
||||
return tail(Storage::disk($this->disk)->path($this->name), $lines);
|
||||
}
|
||||
|
||||
return Storage::disk($this->disk)->get($this->name);
|
||||
}
|
||||
|
||||
|
@ -102,6 +102,21 @@ public static function boot(): void
|
||||
});
|
||||
}
|
||||
|
||||
public function isReady(): bool
|
||||
{
|
||||
return $this->status === SiteStatus::READY;
|
||||
}
|
||||
|
||||
public function isInstalling(): bool
|
||||
{
|
||||
return in_array($this->status, [SiteStatus::INSTALLING, SiteStatus::INSTALLATION_FAILED]);
|
||||
}
|
||||
|
||||
public function isInstallationFailed(): bool
|
||||
{
|
||||
return $this->status === SiteStatus::INSTALLATION_FAILED;
|
||||
}
|
||||
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
|
@ -15,6 +15,7 @@
|
||||
* @property ?string $url
|
||||
* @property string $access_token
|
||||
* @property ?int $project_id
|
||||
* @property string $image_url
|
||||
*/
|
||||
class SourceControl extends AbstractModel
|
||||
{
|
||||
@ -63,4 +64,9 @@ public static function getByProjectId(int $projectId): Builder
|
||||
->where('project_id', $projectId)
|
||||
->orWhereNull('project_id');
|
||||
}
|
||||
|
||||
public function getImageUrlAttribute(): string
|
||||
{
|
||||
return url('/static/images/'.$this->provider.'.svg');
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,9 @@ public function delete(User $user, ServerLog $serverLog): bool
|
||||
{
|
||||
return $user->isAdmin() || $serverLog->server->project->users->contains($user);
|
||||
}
|
||||
|
||||
public function deleteMany(User $user, Server $server): bool
|
||||
{
|
||||
return $user->isAdmin() || $server->project->users->contains($user);
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ public function viewAny(User $user, Server $server): bool
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
|
||||
}
|
||||
|
||||
public function view(User $user, Site $site): bool
|
||||
public function view(User $user, Site $site, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||
$site->server_id === $server->id &&
|
||||
$site->server->isReady();
|
||||
}
|
||||
|
||||
@ -27,15 +28,17 @@ public function create(User $user, Server $server): bool
|
||||
return ($user->isAdmin() || $server->project->users->contains($user)) && $server->isReady();
|
||||
}
|
||||
|
||||
public function update(User $user, Site $site): bool
|
||||
public function update(User $user, Site $site, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||
$site->server_id === $server->id &&
|
||||
$site->server->isReady();
|
||||
}
|
||||
|
||||
public function delete(User $user, Site $site): bool
|
||||
public function delete(User $user, Site $site, Server $server): bool
|
||||
{
|
||||
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||
$site->server_id === $server->id &&
|
||||
$site->server->isReady();
|
||||
}
|
||||
}
|
||||
|
37
app/Policies/SourceControlPolicy.php
Normal file
37
app/Policies/SourceControlPolicy.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\SourceControl;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class SourceControlPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function view(User $user, SourceControl $sourceControl): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function update(User $user, SourceControl $sourceControl): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function delete(User $user, SourceControl $sourceControl): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
}
|
@ -9,7 +9,9 @@
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Panel;
|
||||
use Filament\Support\Assets\Js;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Facades\FilamentAsset;
|
||||
use Filament\Support\Facades\FilamentColor;
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Filament\View\PanelsRenderHook;
|
||||
@ -19,6 +21,7 @@
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use Livewire\Livewire;
|
||||
@ -43,9 +46,12 @@ public function boot(): void
|
||||
PanelsRenderHook::SIDEBAR_FOOTER,
|
||||
fn () => view('web.components.app-version')
|
||||
);
|
||||
FilamentAsset::register([
|
||||
Js::make('app', Vite::asset('resources/js/app.js'))->module(),
|
||||
]);
|
||||
FilamentColor::register([
|
||||
'slate' => Color::Slate,
|
||||
'gray' => Color::Gray,
|
||||
'gray' => Color::Zinc,
|
||||
'red' => Color::Red,
|
||||
'orange' => Color::Orange,
|
||||
'amber' => Color::Amber,
|
||||
@ -97,7 +103,6 @@ public function panel(Panel $panel): Panel
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
])
|
||||
->spa()
|
||||
->login()
|
||||
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
|
||||
->sidebarCollapsibleOnDesktop()
|
||||
|
@ -6,6 +6,7 @@
|
||||
use App\Exceptions\SourceControlIsNotConnected;
|
||||
use App\SSH\Composer\Composer;
|
||||
use App\SSH\Git\Git;
|
||||
use App\SSH\Services\Webserver\Webserver;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PHPSite extends AbstractSiteType
|
||||
@ -36,12 +37,18 @@ public function createRules(array $input): array
|
||||
'required',
|
||||
Rule::exists('source_controls', 'id'),
|
||||
],
|
||||
'web_directory' => [
|
||||
'nullable',
|
||||
],
|
||||
'repository' => [
|
||||
'required',
|
||||
],
|
||||
'branch' => [
|
||||
'required',
|
||||
],
|
||||
'composer' => [
|
||||
'nullable',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -53,6 +60,7 @@ public function createFields(array $input): array
|
||||
'repository' => $input['repository'] ?? '',
|
||||
'branch' => $input['branch'] ?? '',
|
||||
'php_version' => $input['php_version'] ?? '',
|
||||
'composer' => $input['php_version'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Exceptions\SSHError;
|
||||
use App\Helpers\HtmxResponse;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
function generate_public_key($privateKeyPath, $publicKeyPath): void
|
||||
{
|
||||
@ -55,3 +57,84 @@ function get_public_key_content(): string
|
||||
->replace("\n", '')
|
||||
->toString();
|
||||
}
|
||||
|
||||
function run_action(object $static, Closure $callback): void
|
||||
{
|
||||
try {
|
||||
$callback();
|
||||
} catch (SSHError $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->body($e->getLog()?->getContent(10))
|
||||
->send();
|
||||
|
||||
if (method_exists($static, 'halt')) {
|
||||
$reflectionMethod = new ReflectionMethod($static, 'halt');
|
||||
$reflectionMethod->invoke($static);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit: https://gist.github.com/lorenzos/1711e81a9162320fde20
|
||||
*/
|
||||
function tail($filepath, $lines = 1, $adaptive = true): string
|
||||
{
|
||||
// Open file
|
||||
$f = @fopen($filepath, 'rb');
|
||||
if ($f === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Sets buffer size, according to the number of lines to retrieve.
|
||||
// This gives a performance boost when reading a few lines from the file.
|
||||
if (! $adaptive) {
|
||||
$buffer = 4096;
|
||||
} else {
|
||||
$buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
|
||||
}
|
||||
|
||||
// Jump to last character
|
||||
fseek($f, -1, SEEK_END);
|
||||
|
||||
// Read it and adjust line number if necessary
|
||||
// (Otherwise the result would be wrong if file doesn't end with a blank line)
|
||||
if (fread($f, 1) != "\n") {
|
||||
$lines -= 1;
|
||||
}
|
||||
|
||||
// Start reading
|
||||
$output = '';
|
||||
$chunk = '';
|
||||
|
||||
// While we would like more
|
||||
while (ftell($f) > 0 && $lines >= 0) {
|
||||
// Figure out how far back we should jump
|
||||
$seek = min(ftell($f), $buffer);
|
||||
|
||||
// Do the jump (backwards, relative to where we are)
|
||||
fseek($f, -$seek, SEEK_CUR);
|
||||
|
||||
// Read a chunk and prepend it to our output
|
||||
$output = ($chunk = fread($f, $seek)).$output;
|
||||
|
||||
// Jump back to where we started reading
|
||||
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
|
||||
|
||||
// Decrease our line counter
|
||||
$lines -= substr_count($chunk, "\n");
|
||||
}
|
||||
|
||||
// While we have too many lines
|
||||
// (Because of buffer size we might have read too many)
|
||||
while ($lines++ < 0) {
|
||||
// Find first newline and remove all text before that
|
||||
$output = substr($output, strpos($output, "\n") + 1);
|
||||
}
|
||||
|
||||
// Close file and return
|
||||
fclose($f);
|
||||
|
||||
return trim($output);
|
||||
}
|
||||
|
@ -2,12 +2,40 @@
|
||||
|
||||
namespace App\Web\Components;
|
||||
|
||||
use App\Web\Traits\HasWidgets;
|
||||
use Filament\Pages\Page as BasePage;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
abstract class Page extends BasePage
|
||||
{
|
||||
use HasWidgets;
|
||||
|
||||
protected static string $view = 'web.components.page';
|
||||
|
||||
protected ?string $live = '5s';
|
||||
|
||||
protected array $extraAttributes = [];
|
||||
|
||||
protected function getExtraAttributes(): array
|
||||
{
|
||||
$attributes = $this->extraAttributes;
|
||||
|
||||
if ($this->getLive()) {
|
||||
$attributes['wire:poll.'.$this->getLive()] = '$dispatch(\'$refresh\')';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
public function getExtraAttributesBag(): ComponentAttributeBag
|
||||
{
|
||||
return new ComponentAttributeBag($this->getExtraAttributes());
|
||||
}
|
||||
|
||||
public function getLive(): ?string
|
||||
{
|
||||
return $this->live;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
24
app/Web/Fields/CodeEditorField.php
Normal file
24
app/Web/Fields/CodeEditorField.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Fields;
|
||||
|
||||
use Filament\Forms\Components\Field;
|
||||
|
||||
class CodeEditorField extends Field
|
||||
{
|
||||
protected string $view = 'web.fields.code-editor';
|
||||
|
||||
public string $lang = '';
|
||||
|
||||
public bool $readonly = false;
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->getName(),
|
||||
'lang' => $this->lang,
|
||||
'value' => json_encode($this->getState() ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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', [
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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])),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Traits;
|
||||
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;
|
||||
@ -13,15 +14,19 @@
|
||||
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\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;
|
||||
|
||||
trait PageHasServer
|
||||
abstract class Page extends BasePage
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public function getSubNavigation(): array
|
||||
{
|
||||
$items = [];
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
83
app/Web/Pages/Servers/Sites/Page.php
Normal file
83
app/Web/Pages/Servers/Sites/Page.php
Normal 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;
|
||||
}
|
||||
}
|
22
app/Web/Pages/Servers/Sites/Pages/Queues/Index.php
Normal file
22
app/Web/Pages/Servers/Sites/Pages/Queues/Index.php
Normal 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 [];
|
||||
}
|
||||
}
|
22
app/Web/Pages/Servers/Sites/Pages/SSL/Index.php
Normal file
22
app/Web/Pages/Servers/Sites/Pages/SSL/Index.php
Normal 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 [];
|
||||
}
|
||||
}
|
198
app/Web/Pages/Servers/Sites/View.php
Normal file
198
app/Web/Pages/Servers/Sites/View.php
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
57
app/Web/Pages/Servers/Sites/Widgets/Installing.php
Normal file
57
app/Web/Pages/Servers/Sites/Widgets/Installing.php
Normal 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());
|
||||
}
|
||||
}
|
55
app/Web/Pages/Servers/Sites/Widgets/SiteSummary.php
Normal file
55
app/Web/Pages/Servers/Sites/Widgets/SiteSummary.php
Normal 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());
|
||||
}
|
||||
}
|
@ -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) => '/'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
use App\Actions\Projects\DeleteProject;
|
||||
use App\Models\Project;
|
||||
use App\Web\Components\Page;
|
||||
use App\Web\Pages\Servers\Page;
|
||||
use App\Web\Pages\Settings\Projects\Widgets\AddUser;
|
||||
use App\Web\Pages\Settings\Projects\Widgets\ProjectUsersList;
|
||||
use App\Web\Pages\Settings\Projects\Widgets\UpdateProject;
|
||||
@ -19,8 +19,6 @@ class Settings extends Page
|
||||
|
||||
protected static ?string $title = 'Project Settings';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('update', request()->route('project')) ?? false;
|
||||
|
69
app/Web/Pages/Settings/SourceControls/Actions/Create.php
Normal file
69
app/Web/Pages/Settings/SourceControls/Actions/Create.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\SourceControls\Actions;
|
||||
|
||||
use App\Actions\SourceControl\ConnectSourceControl;
|
||||
use App\Enums\SourceControl;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class Create
|
||||
{
|
||||
public static function form(): array
|
||||
{
|
||||
return [
|
||||
Select::make('provider')
|
||||
->options(
|
||||
collect(config('core.source_control_providers'))
|
||||
->mapWithKeys(fn ($provider) => [$provider => $provider])
|
||||
)
|
||||
->live()
|
||||
->reactive()
|
||||
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['provider']),
|
||||
TextInput::make('name')
|
||||
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['name']),
|
||||
TextInput::make('token')
|
||||
->label('API Key')
|
||||
->validationAttribute('API Key')
|
||||
->visible(fn ($get) => in_array($get('provider'), [
|
||||
SourceControl::GITHUB,
|
||||
SourceControl::GITLAB,
|
||||
]))
|
||||
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['token']),
|
||||
TextInput::make('url')
|
||||
->label('URL (optional)')
|
||||
->visible(fn ($get) => $get('provider') == SourceControl::GITLAB)
|
||||
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['url'])
|
||||
->helperText('If you run a self-managed gitlab enter the url here, leave empty to use gitlab.com'),
|
||||
TextInput::make('username')
|
||||
->visible(fn ($get) => $get('provider') == SourceControl::BITBUCKET)
|
||||
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['username']),
|
||||
TextInput::make('password')
|
||||
->visible(fn ($get) => $get('provider') == SourceControl::BITBUCKET)
|
||||
->rules(fn (Get $get) => ConnectSourceControl::rules($get())['password']),
|
||||
Checkbox::make('global')
|
||||
->label('Is Global (Accessible in all projects)'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function action(array $data): void
|
||||
{
|
||||
try {
|
||||
app(ConnectSourceControl::class)->connect(auth()->user(), $data);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
33
app/Web/Pages/Settings/SourceControls/Actions/Edit.php
Normal file
33
app/Web/Pages/Settings/SourceControls/Actions/Edit.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\SourceControls\Actions;
|
||||
|
||||
use App\Actions\SourceControl\EditSourceControl;
|
||||
use App\Models\SourceControl;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class Edit
|
||||
{
|
||||
public static function form(): array
|
||||
{
|
||||
return Create::form();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function action(SourceControl $provider, array $data): void
|
||||
{
|
||||
try {
|
||||
app(EditSourceControl::class)->edit($provider, auth()->user(), $data);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
52
app/Web/Pages/Settings/SourceControls/Index.php
Normal file
52
app/Web/Pages/Settings/SourceControls/Index.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\SourceControls;
|
||||
|
||||
use App\Models\SourceControl;
|
||||
use App\Web\Components\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?string $slug = 'settings/source-controls';
|
||||
|
||||
protected static ?string $title = 'Source Controls';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-code-bracket';
|
||||
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('viewAny', SourceControl::class) ?? false;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\SourceControlsList::class],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('connect')
|
||||
->label('Connect')
|
||||
->icon('heroicon-o-wifi')
|
||||
->modalHeading('Connect to a Source Control')
|
||||
->modalSubmitActionLabel('Connect')
|
||||
->form(Actions\Create::form())
|
||||
->authorize('create', SourceControl::class)
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->action(function (array $data) {
|
||||
Actions\Create::action($data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\SourceControls\Widgets;
|
||||
|
||||
use App\Actions\SourceControl\DeleteSourceControl;
|
||||
use App\Models\SourceControl;
|
||||
use App\Web\Pages\Settings\SourceControls\Actions\Edit;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SourceControlsList extends Widget
|
||||
{
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return SourceControl::getByProjectId(auth()->user()->current_project_id);
|
||||
}
|
||||
|
||||
protected static ?string $heading = '';
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
ImageColumn::make('image_url')
|
||||
->label('Provider')
|
||||
->size(24),
|
||||
TextColumn::make('name')
|
||||
->default(fn (SourceControl $record) => $record->profile)
|
||||
->label('Name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('id')
|
||||
->label('Global')
|
||||
->badge()
|
||||
->color(fn (SourceControl $record) => $record->project_id ? 'gray' : 'success')
|
||||
->formatStateUsing(function (SourceControl $record) {
|
||||
return $record->project_id ? 'No' : 'Yes';
|
||||
}),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created At')
|
||||
->formatStateUsing(fn (SourceControl $record) => $record->created_at_by_timezone)
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table->actions([
|
||||
EditAction::make('edit')
|
||||
->label('Edit')
|
||||
->modalHeading('Edit Source Control')
|
||||
->mutateRecordDataUsing(function (array $data, SourceControl $record) {
|
||||
return [
|
||||
'name' => $record->profile,
|
||||
'token' => $record->provider_data['token'] ?? null,
|
||||
'username' => $record->provider_data['username'] ?? null,
|
||||
'password' => $record->provider_data['password'] ?? null,
|
||||
'global' => $record->project_id === null,
|
||||
];
|
||||
})
|
||||
->form(Edit::form())
|
||||
->authorize(fn (SourceControl $record) => auth()->user()->can('update', $record))
|
||||
->using(fn (array $data, SourceControl $record) => Edit::action($record, $data))
|
||||
->modalWidth(MaxWidth::Medium),
|
||||
DeleteAction::make('delete')
|
||||
->label('Delete')
|
||||
->modalHeading('Delete Source Control')
|
||||
->authorize(fn (SourceControl $record) => auth()->user()->can('delete', $record))
|
||||
->using(function (array $data, SourceControl $record) {
|
||||
app(DeleteSourceControl::class)->delete($record);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Traits;
|
||||
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
trait HasWidgets
|
||||
{
|
||||
protected ?string $live = '5s';
|
||||
|
||||
protected array $extraAttributes = [];
|
||||
|
||||
protected function getExtraAttributes(): array
|
||||
{
|
||||
$attributes = $this->extraAttributes;
|
||||
|
||||
if ($this->getLive()) {
|
||||
$attributes['wire:poll.'.$this->getLive()] = '$dispatch(\'$refresh\')';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
public function getExtraAttributesBag(): ComponentAttributeBag
|
||||
{
|
||||
return new ComponentAttributeBag($this->getExtraAttributes());
|
||||
}
|
||||
|
||||
public function getLive(): ?string
|
||||
{
|
||||
return $this->live;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user