- 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

@ -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);
}
}

View File

@ -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();
}
}

View File

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

View File

@ -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'],
];
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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,
];
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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();
});
});
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View 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();
}
}

View File

@ -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()

View File

@ -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'] ?? '',
];
}

View File

@ -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);
}

View File

@ -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 [];
}
}

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

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,7 +61,6 @@ 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(
@ -69,7 +68,6 @@ public function form(Form $form): Form
content: $output,
);
});
})->name($log);
}),
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');
});
}),
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');
});
}),
]);
}

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,12 +22,20 @@ 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
{
@ -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

@ -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 = [];

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);

View File

@ -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;

View 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;
}
}
}

View 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;
}
}
}

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

View File

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

View File

@ -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 [];
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('databases', function (Blueprint $table) {
$table->softDeletes();
});
}
public function down(): void
{
Schema::table('databases', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="250px" viewBox="0 0 256 250" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<svg width="256px" height="250px" viewBox="0 0 256 250" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="current"></path>
<path
d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z"
fill="#3E75C3"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -11,13 +11,18 @@ .choices__item--selectable {
}
.fi-sidebar {
@apply bg-gray-100/50 dark:bg-gray-900/50 !important;
@apply bg-gray-100 dark:bg-gray-900 !important;
}
.fi-sidebar-item a, .fi-tenant-menu-trigger {
@apply hover:bg-gray-200/50 hover:dark:bg-gray-800 !important;
}
.fi-sidebar-item-active a {
@apply bg-gray-100 dark:bg-gray-800/50 !important;
@apply bg-gray-200/50 dark:bg-gray-800 !important;
}
.fi-btn-color-primary {
background-image: linear-gradient(to bottom right, rgba(var(--primary-500), 1), rgba(var(--primary-900), 1));
box-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;

View File

@ -1,40 +1,40 @@
import ace from 'brace';
import 'brace/mode/javascript';
import 'brace/mode/plain_text';
import 'brace/mode/sh';
import 'brace/mode/ini';
import 'brace/ext/searchbox'
import './theme-vito'
import './mode-env';
import './mode-nginx';
import ace from "brace";
import "brace/mode/javascript";
import "brace/mode/plain_text";
import "brace/mode/sh";
import "brace/mode/ini";
import "brace/ext/searchbox";
import "./theme-vito";
import "./mode-env";
import "./mode-nginx";
window.initAceEditor = function (options = {}) {
const editorValue = JSON.parse(options.value || '');
const editorValue = JSON.parse(options.value || "");
const editor = ace.edit(options.id);
editor.setTheme("ace/theme/vito");
editor.getSession().setMode(`ace/mode/${options.lang || 'plain_text'}`);
editor.setValue(editorValue, -1);
editor.clearSelection();
editor.focus();
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
// enableBasicAutocompletion: true,
// enableSnippets: true,
// enableLiveAutocompletion: true,
printMargin: false,
});
editor.renderer.setScrollMargin(15, 15, 0, 0)
editor.renderer.setScrollMargin(15, 15, 0, 0);
editor.renderer.setPadding(15);
editor.getSession().on('change', function () {
document.getElementById(`textarea-${options.id}`).value = editor.getValue();
editor.getSession().on("change", function () {
document.getElementById(`textarea-${options.id}`).value =
editor.getValue();
});
window.addEventListener('resize', function () {
window.addEventListener("resize", function () {
editor.resize();
})
});
document.getElementById(`textarea-${options.id}`).innerHTML = editorValue;
return editor;
}
};

View File

@ -1,62 +1,7 @@
import 'flowbite';
import 'flowbite/dist/datepicker.js';
import './ace-editor/ace-editor';
import CodeEditorAlpinePlugin from "./components/editor";
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
import ApexCharts from 'apexcharts';
window.ApexCharts = ApexCharts;
import htmx from "htmx.org";
window.htmx = htmx;
window.htmx.defineExtension('disable-element', {
onEvent: function (name, evt) {
let elt = evt.detail.elt;
let target = elt.getAttribute("hx-disable-element");
let targetElements = (target === "self") ? [elt] : document.querySelectorAll(target);
for (let i = 0; i < targetElements.length; i++) {
if (name === "htmx:beforeRequest" && targetElements[i]) {
targetElements[i].disabled = true;
} else if (name === "htmx:afterRequest" && targetElements[i]) {
targetElements[i].disabled = false;
}
}
}
});
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRF-TOKEN'] = document.head.querySelector('meta[name="csrf-token"]').content;
// if (window.getSelection) { window.getSelection().removeAllRanges(); }
// else if (document.selection) { document.selection.empty(); }
});
document.body.addEventListener('htmx:beforeRequest', (event) => {
let targetElements = event.target.querySelectorAll('[hx-disable]');
for (let i = 0; i < targetElements.length; i++) {
targetElements[i].disabled = true;
}
});
document.body.addEventListener('htmx:afterRequest', (event) => {
let targetElements = event.target.querySelectorAll('[hx-disable]');
for (let i = 0; i < targetElements.length; i++) {
targetElements[i].disabled = false;
}
});
document.body.addEventListener('htmx:afterSwap', (event) => {
tippy('[data-tooltip]', {
content(reference) {
return reference.getAttribute('data-tooltip');
},
});
});
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
tippy('[data-tooltip]', {
content(reference) {
return reference.getAttribute('data-tooltip');
},
document.addEventListener("alpine:init", () => {
window.Alpine.plugin(CodeEditorAlpinePlugin);
});
window.copyToClipboard = async function (text) {
@ -73,11 +18,11 @@ window.copyToClipboard = async function (text) {
textArea.select();
try {
document.execCommand('copy');
document.execCommand("copy");
} catch (error) {
//
} finally {
textArea.remove();
}
}
}
};

View File

@ -0,0 +1,48 @@
import ace from "brace";
import "brace/mode/ini";
import "brace/ext/searchbox";
import "../ace-editor/theme-vito";
import "../ace-editor/mode-env";
import "../ace-editor/mode-nginx";
export default (Alpine) => {
Alpine.data("codeEditorFormComponent", ({ state, options }) => {
return {
state,
options,
init: function () {
this.render();
},
render() {
this.editor = null;
const editorValue = JSON.parse(this.options.value || "");
this.editor = ace.edit(this.options.id);
this.editor.$blockScrolling = Infinity;
this.editor.setTheme("ace/theme/vito");
this.editor.setValue(editorValue, -1);
this.editor
.getSession()
.setMode(`ace/mode/${this.options.lang || "plain_text"}`);
this.editor.clearSelection();
this.editor.focus();
this.editor.setOptions({
printMargin: false,
});
this.editor.renderer.setScrollMargin(15, 15, 0, 0);
this.editor.renderer.setPadding(15);
this.editor.getSession().on("change", () => {
this.state = this.editor.getValue();
});
window.addEventListener("resize", () => {
this.editor.resize();
});
this.state = editorValue;
},
};
});
};

View File

@ -1,5 +1,5 @@
<div>
<form wire:submit="submit">
<form>
{{ $this->form }}
</form>
<x-filament-actions::modals />

View File

@ -1,13 +1,11 @@
<div {{ $this->getExtraAttributesBag() }}>
<x-filament-panels::page>
@if (method_exists($this, "getSecondSubNavigation"))
@if (method_exists($this, "getSecondSubNavigation") && count($this->getSecondSubNavigation()) > 0)
<x-filament-panels::page.sub-navigation.tabs class="!flex" :navigation="$this->getSecondSubNavigation()" />
@endif
@foreach ($this->getWidgets() as $key => $widget)
@livewire($widget[0], $widget[1] ?? [], key(class_basename($widget[0]) . "-" . $key))
@endforeach
<x-filament-actions::modals />
</x-filament-panels::page>
</div>

View File

@ -0,0 +1,18 @@
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field" :label-sr-only="$isLabelHidden()">
<div
wire:ignore
x-data="codeEditorFormComponent({
state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }},
options: @js($getOptions()),
})"
>
<div>
<div
id="{{ $getId() }}"
{{ $attributes->merge(["class" => "mt-1 min-h-[400px] w-full border border-gray-100 dark:border-gray-700"]) }}
class="ace-vito ace_dark"
></div>
<textarea id="textarea-{{ $getId() }}" style="display: none" x-model="state"></textarea>
</div>
</div>
</x-dynamic-component>