mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-20 18:31:36 +00:00
- 2.x - sites (wip)
- improved ssh error handling - database soft deletes
This commit is contained in:
parent
ecdba02bc9
commit
d1f2add699
@ -14,7 +14,6 @@
|
|||||||
use App\ValidationRules\DomainRule;
|
use App\ValidationRules\DomainRule;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
@ -25,8 +24,6 @@ class CreateSite
|
|||||||
*/
|
*/
|
||||||
public function create(Server $server, array $input): Site
|
public function create(Server $server, array $input): Site
|
||||||
{
|
{
|
||||||
$this->validateInputs($server, $input);
|
|
||||||
|
|
||||||
DB::beginTransaction();
|
DB::beginTransaction();
|
||||||
try {
|
try {
|
||||||
$site = new Site([
|
$site = new Site([
|
||||||
@ -60,9 +57,6 @@ public function create(Server $server, array $input): Site
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate type
|
|
||||||
$this->validateType($site, $input);
|
|
||||||
|
|
||||||
// set type data
|
// set type data
|
||||||
$site->type_data = $site->type()->data($input);
|
$site->type_data = $site->type()->data($input);
|
||||||
|
|
||||||
@ -101,13 +95,9 @@ public function create(Server $server, array $input): Site
|
|||||||
/**
|
/**
|
||||||
* @throws ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public static function rules(array $input): void
|
public static function rules(Server $server, array $input): array
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'server_id' => [
|
|
||||||
'required',
|
|
||||||
'exists:servers,id',
|
|
||||||
],
|
|
||||||
'type' => [
|
'type' => [
|
||||||
'required',
|
'required',
|
||||||
Rule::in(config('core.site_types')),
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static function typeRules(Server $server, array $input): array
|
||||||
* @throws ValidationException
|
|
||||||
*/
|
|
||||||
private function validateType(Site $site, array $input): void
|
|
||||||
{
|
{
|
||||||
$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;
|
namespace App\Actions\Site;
|
||||||
|
|
||||||
use App\Models\Site;
|
use App\Models\Site;
|
||||||
|
use App\SSH\Services\Webserver\Webserver;
|
||||||
|
|
||||||
class DeleteSite
|
class DeleteSite
|
||||||
{
|
{
|
||||||
public function delete(Site $site): void
|
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();
|
$site->delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
use App\Models\Site;
|
use App\Models\Site;
|
||||||
use App\SSH\Git\Git;
|
use App\SSH\Git\Git;
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class UpdateBranch
|
class UpdateBranch
|
||||||
@ -14,7 +13,6 @@ class UpdateBranch
|
|||||||
*/
|
*/
|
||||||
public function update(Site $site, array $input): void
|
public function update(Site $site, array $input): void
|
||||||
{
|
{
|
||||||
$this->validate($input);
|
|
||||||
$site->branch = $input['branch'];
|
$site->branch = $input['branch'];
|
||||||
app(Git::class)->checkout($site);
|
app(Git::class)->checkout($site);
|
||||||
$site->save();
|
$site->save();
|
||||||
@ -23,10 +21,10 @@ public function update(Site $site, array $input): void
|
|||||||
/**
|
/**
|
||||||
* @throws ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
protected function validate(array $input): void
|
public static function rules(): array
|
||||||
{
|
{
|
||||||
Validator::make($input, [
|
return [
|
||||||
'branch' => 'required',
|
'branch' => 'required',
|
||||||
]);
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
namespace App\Actions\Site;
|
namespace App\Actions\Site;
|
||||||
|
|
||||||
use App\Models\Site;
|
use App\Models\Site;
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class UpdateDeploymentScript
|
class UpdateDeploymentScript
|
||||||
@ -13,8 +12,6 @@ class UpdateDeploymentScript
|
|||||||
*/
|
*/
|
||||||
public function update(Site $site, array $input): void
|
public function update(Site $site, array $input): void
|
||||||
{
|
{
|
||||||
$this->validate($input);
|
|
||||||
|
|
||||||
$site->deploymentScript()->update([
|
$site->deploymentScript()->update([
|
||||||
'content' => $input['script'],
|
'content' => $input['script'],
|
||||||
]);
|
]);
|
||||||
@ -23,10 +20,10 @@ public function update(Site $site, array $input): void
|
|||||||
/**
|
/**
|
||||||
* @throws ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
protected function validate(array $input): void
|
public static function rules(): array
|
||||||
{
|
{
|
||||||
Validator::make($input, [
|
return [
|
||||||
'script' => 'required',
|
'script' => ['required', 'string'],
|
||||||
]);
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
use App\Models\SourceControl;
|
use App\Models\SourceControl;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
@ -13,8 +12,6 @@ class ConnectSourceControl
|
|||||||
{
|
{
|
||||||
public function connect(User $user, array $input): void
|
public function connect(User $user, array $input): void
|
||||||
{
|
{
|
||||||
$this->validate($input);
|
|
||||||
|
|
||||||
$sourceControl = new SourceControl([
|
$sourceControl = new SourceControl([
|
||||||
'provider' => $input['provider'],
|
'provider' => $input['provider'],
|
||||||
'profile' => $input['name'],
|
'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,
|
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->validateProvider($sourceControl, $input);
|
|
||||||
|
|
||||||
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
|
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
|
||||||
|
|
||||||
if (! $sourceControl->provider()->connect()) {
|
if (! $sourceControl->provider()->connect()) {
|
||||||
@ -36,28 +31,34 @@ public function connect(User $user, array $input): void
|
|||||||
$sourceControl->save();
|
$sourceControl->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static function rules(array $input): array
|
||||||
* @throws ValidationException
|
|
||||||
*/
|
|
||||||
private function validate(array $input): void
|
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
],
|
||||||
'provider' => [
|
'provider' => [
|
||||||
'required',
|
'required',
|
||||||
Rule::in(config('core.source_control_providers')),
|
Rule::in(config('core.source_control_providers')),
|
||||||
],
|
],
|
||||||
'name' => [
|
|
||||||
'required',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
Validator::make($input, $rules)->validate();
|
|
||||||
|
return array_merge($rules, static::providerRules($input));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws ValidationException
|
* @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\SourceControl;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class EditSourceControl
|
class EditSourceControl
|
||||||
{
|
{
|
||||||
public function edit(SourceControl $sourceControl, User $user, array $input): void
|
public function edit(SourceControl $sourceControl, User $user, array $input): void
|
||||||
{
|
{
|
||||||
$this->validate($input);
|
|
||||||
|
|
||||||
$sourceControl->profile = $input['name'];
|
$sourceControl->profile = $input['name'];
|
||||||
$sourceControl->url = $input['url'] ?? null;
|
$sourceControl->url = $input['url'] ?? null;
|
||||||
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
|
$sourceControl->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
|
||||||
|
|
||||||
$this->validateProvider($sourceControl, $input);
|
|
||||||
|
|
||||||
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
|
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
|
||||||
|
|
||||||
if (! $sourceControl->provider()->connect()) {
|
if (! $sourceControl->provider()->connect()) {
|
||||||
@ -31,24 +27,34 @@ public function edit(SourceControl $sourceControl, User $user, array $input): vo
|
|||||||
$sourceControl->save();
|
$sourceControl->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static function rules(array $input): array
|
||||||
* @throws ValidationException
|
|
||||||
*/
|
|
||||||
private function validate(array $input): void
|
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'name' => [
|
'name' => [
|
||||||
'required',
|
'required',
|
||||||
],
|
],
|
||||||
|
'provider' => [
|
||||||
|
'required',
|
||||||
|
Rule::in(config('core.source_control_providers')),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
Validator::make($input, $rules)->validate();
|
|
||||||
|
return array_merge($rules, static::providerRules($input));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws ValidationException
|
* @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 WORDPRESS = 'wordpress';
|
||||||
|
|
||||||
const PHPMYADMIN = 'phpmyadmin';
|
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;
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use App\Models\ServerLog;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class SSHError extends Exception
|
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
|
public function exec(string $command, string $log = '', ?int $siteId = null, ?bool $stream = false, ?callable $streamCallback = null): string
|
||||||
{
|
{
|
||||||
|
ds($command);
|
||||||
if (! $this->log && $log) {
|
if (! $this->log && $log) {
|
||||||
$this->log = ServerLog::make($this->server, $log);
|
$this->log = ServerLog::make($this->server, $log);
|
||||||
if ($siteId) {
|
if ($siteId) {
|
||||||
@ -129,13 +130,19 @@ public function exec(string $command, string $log = '', ?int $siteId = null, ?bo
|
|||||||
$this->log?->write($output);
|
$this->log?->write($output);
|
||||||
|
|
||||||
if ($this->connection->getExitStatus() !== 0 || Str::contains($output, 'VITO_SSH_ERROR')) {
|
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;
|
return $output;
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} 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
|
public function database(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Database::class);
|
return $this->belongsTo(Database::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function files(): HasMany
|
public function files(): HasMany
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\DatabaseStatus;
|
use App\Enums\DatabaseStatus;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property int $server_id
|
* @property int $server_id
|
||||||
@ -13,10 +15,12 @@
|
|||||||
* @property string $status
|
* @property string $status
|
||||||
* @property Server $server
|
* @property Server $server
|
||||||
* @property Backup[] $backups
|
* @property Backup[] $backups
|
||||||
|
* @property Carbon $deleted_at
|
||||||
*/
|
*/
|
||||||
class Database extends AbstractModel
|
class Database extends AbstractModel
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'server_id',
|
'server_id',
|
||||||
@ -41,9 +45,6 @@ public static function boot(): void
|
|||||||
$user->save();
|
$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) {
|
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 (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);
|
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
|
public function server(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Server::class);
|
return $this->belongsTo(Server::class);
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
* @property ?string $url
|
* @property ?string $url
|
||||||
* @property string $access_token
|
* @property string $access_token
|
||||||
* @property ?int $project_id
|
* @property ?int $project_id
|
||||||
|
* @property string $image_url
|
||||||
*/
|
*/
|
||||||
class SourceControl extends AbstractModel
|
class SourceControl extends AbstractModel
|
||||||
{
|
{
|
||||||
@ -63,4 +64,9 @@ public static function getByProjectId(int $projectId): Builder
|
|||||||
->where('project_id', $projectId)
|
->where('project_id', $projectId)
|
||||||
->orWhereNull('project_id');
|
->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);
|
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();
|
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)) &&
|
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||||
|
$site->server_id === $server->id &&
|
||||||
$site->server->isReady();
|
$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();
|
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)) &&
|
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||||
|
$site->server_id === $server->id &&
|
||||||
$site->server->isReady();
|
$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)) &&
|
return ($user->isAdmin() || $site->server->project->users->contains($user)) &&
|
||||||
|
$site->server_id === $server->id &&
|
||||||
$site->server->isReady();
|
$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\DisableBladeIconComponents;
|
||||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
|
use Filament\Support\Assets\Js;
|
||||||
use Filament\Support\Colors\Color;
|
use Filament\Support\Colors\Color;
|
||||||
|
use Filament\Support\Facades\FilamentAsset;
|
||||||
use Filament\Support\Facades\FilamentColor;
|
use Filament\Support\Facades\FilamentColor;
|
||||||
use Filament\Support\Facades\FilamentView;
|
use Filament\Support\Facades\FilamentView;
|
||||||
use Filament\View\PanelsRenderHook;
|
use Filament\View\PanelsRenderHook;
|
||||||
@ -19,6 +21,7 @@
|
|||||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||||
use Illuminate\Session\Middleware\AuthenticateSession;
|
use Illuminate\Session\Middleware\AuthenticateSession;
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Illuminate\Support\Facades\Vite;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
@ -43,9 +46,12 @@ public function boot(): void
|
|||||||
PanelsRenderHook::SIDEBAR_FOOTER,
|
PanelsRenderHook::SIDEBAR_FOOTER,
|
||||||
fn () => view('web.components.app-version')
|
fn () => view('web.components.app-version')
|
||||||
);
|
);
|
||||||
|
FilamentAsset::register([
|
||||||
|
Js::make('app', Vite::asset('resources/js/app.js'))->module(),
|
||||||
|
]);
|
||||||
FilamentColor::register([
|
FilamentColor::register([
|
||||||
'slate' => Color::Slate,
|
'slate' => Color::Slate,
|
||||||
'gray' => Color::Gray,
|
'gray' => Color::Zinc,
|
||||||
'red' => Color::Red,
|
'red' => Color::Red,
|
||||||
'orange' => Color::Orange,
|
'orange' => Color::Orange,
|
||||||
'amber' => Color::Amber,
|
'amber' => Color::Amber,
|
||||||
@ -97,7 +103,6 @@ public function panel(Panel $panel): Panel
|
|||||||
->authMiddleware([
|
->authMiddleware([
|
||||||
Authenticate::class,
|
Authenticate::class,
|
||||||
])
|
])
|
||||||
->spa()
|
|
||||||
->login()
|
->login()
|
||||||
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
|
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
|
||||||
->sidebarCollapsibleOnDesktop()
|
->sidebarCollapsibleOnDesktop()
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
use App\Exceptions\SourceControlIsNotConnected;
|
use App\Exceptions\SourceControlIsNotConnected;
|
||||||
use App\SSH\Composer\Composer;
|
use App\SSH\Composer\Composer;
|
||||||
use App\SSH\Git\Git;
|
use App\SSH\Git\Git;
|
||||||
|
use App\SSH\Services\Webserver\Webserver;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class PHPSite extends AbstractSiteType
|
class PHPSite extends AbstractSiteType
|
||||||
@ -36,12 +37,18 @@ public function createRules(array $input): array
|
|||||||
'required',
|
'required',
|
||||||
Rule::exists('source_controls', 'id'),
|
Rule::exists('source_controls', 'id'),
|
||||||
],
|
],
|
||||||
|
'web_directory' => [
|
||||||
|
'nullable',
|
||||||
|
],
|
||||||
'repository' => [
|
'repository' => [
|
||||||
'required',
|
'required',
|
||||||
],
|
],
|
||||||
'branch' => [
|
'branch' => [
|
||||||
'required',
|
'required',
|
||||||
],
|
],
|
||||||
|
'composer' => [
|
||||||
|
'nullable',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +60,7 @@ public function createFields(array $input): array
|
|||||||
'repository' => $input['repository'] ?? '',
|
'repository' => $input['repository'] ?? '',
|
||||||
'branch' => $input['branch'] ?? '',
|
'branch' => $input['branch'] ?? '',
|
||||||
'php_version' => $input['php_version'] ?? '',
|
'php_version' => $input['php_version'] ?? '',
|
||||||
|
'composer' => $input['php_version'] ?? '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Exceptions\SSHError;
|
||||||
use App\Helpers\HtmxResponse;
|
use App\Helpers\HtmxResponse;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
|
||||||
function generate_public_key($privateKeyPath, $publicKeyPath): void
|
function generate_public_key($privateKeyPath, $publicKeyPath): void
|
||||||
{
|
{
|
||||||
@ -55,3 +57,84 @@ function get_public_key_content(): string
|
|||||||
->replace("\n", '')
|
->replace("\n", '')
|
||||||
->toString();
|
->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;
|
namespace App\Web\Components;
|
||||||
|
|
||||||
use App\Web\Traits\HasWidgets;
|
|
||||||
use Filament\Pages\Page as BasePage;
|
use Filament\Pages\Page as BasePage;
|
||||||
|
use Illuminate\View\ComponentAttributeBag;
|
||||||
|
|
||||||
abstract class Page extends BasePage
|
abstract class Page extends BasePage
|
||||||
{
|
{
|
||||||
use HasWidgets;
|
|
||||||
|
|
||||||
protected static string $view = 'web.components.page';
|
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;
|
namespace App\Web\Pages\Servers\Console;
|
||||||
|
|
||||||
use App\Models\Server;
|
use App\Web\Pages\Servers\Page;
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected ?string $live = '';
|
protected ?string $live = '';
|
||||||
|
|
||||||
protected $listeners = [];
|
protected $listeners = [];
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/console';
|
protected static ?string $slug = 'servers/{server}/console';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Console';
|
protected static ?string $title = 'Console';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('update', static::getServerFromRoute()) ?? false;
|
return auth()->user()?->can('update', static::getServerFromRoute()) ?? false;
|
||||||
|
@ -61,7 +61,6 @@ public function form(Form $form): Form
|
|||||||
$this->running = true;
|
$this->running = true;
|
||||||
$ssh = $this->server->ssh($this->data['user']);
|
$ssh = $this->server->ssh($this->data['user']);
|
||||||
$log = 'console-'.time();
|
$log = 'console-'.time();
|
||||||
defer(function () use ($ssh, $log) {
|
|
||||||
$ssh->exec(command: $this->data['command'], log: $log, stream: true, streamCallback: function ($output) {
|
$ssh->exec(command: $this->data['command'], log: $log, stream: true, streamCallback: function ($output) {
|
||||||
$this->output .= $output;
|
$this->output .= $output;
|
||||||
$this->stream(
|
$this->stream(
|
||||||
@ -69,7 +68,6 @@ public function form(Form $form): Form
|
|||||||
content: $output,
|
content: $output,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
})->name($log);
|
|
||||||
}),
|
}),
|
||||||
Action::make('stop')
|
Action::make('stop')
|
||||||
->view('web.components.dynamic-widget', [
|
->view('web.components.dynamic-widget', [
|
||||||
|
@ -4,9 +4,7 @@
|
|||||||
|
|
||||||
use App\Actions\CronJob\CreateCronJob;
|
use App\Actions\CronJob\CreateCronJob;
|
||||||
use App\Models\CronJob;
|
use App\Models\CronJob;
|
||||||
use App\Models\Server;
|
use App\Web\Pages\Servers\Page;
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
@ -16,18 +14,12 @@
|
|||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/cronjobs';
|
protected static ?string $slug = 'servers/{server}/cronjobs';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Cron Jobs';
|
protected static ?string $title = 'Cron Jobs';
|
||||||
|
|
||||||
protected $listeners = ['$refresh'];
|
protected $listeners = ['$refresh'];
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [CronJob::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [CronJob::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -4,12 +4,10 @@
|
|||||||
|
|
||||||
use App\Actions\Database\CreateBackup;
|
use App\Actions\Database\CreateBackup;
|
||||||
use App\Models\Backup;
|
use App\Models\Backup;
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\StorageProvider;
|
use App\Models\StorageProvider;
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Contracts\HasSecondSubNav;
|
use App\Web\Contracts\HasSecondSubNav;
|
||||||
|
use App\Web\Pages\Servers\Page;
|
||||||
use App\Web\Pages\Settings\StorageProviders\Actions\Create;
|
use App\Web\Pages\Settings\StorageProviders\Actions\Create;
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
@ -19,17 +17,12 @@
|
|||||||
|
|
||||||
class Backups extends Page implements HasSecondSubNav
|
class Backups extends Page implements HasSecondSubNav
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
use Traits\Navigation;
|
use Traits\Navigation;
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/databases/backups';
|
protected static ?string $slug = 'servers/{server}/databases/backups';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Backups';
|
protected static ?string $title = 'Backups';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [Backup::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [Backup::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -4,11 +4,8 @@
|
|||||||
|
|
||||||
use App\Actions\Database\CreateDatabase;
|
use App\Actions\Database\CreateDatabase;
|
||||||
use App\Models\Database;
|
use App\Models\Database;
|
||||||
use App\Models\Server;
|
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Contracts\HasSecondSubNav;
|
use App\Web\Contracts\HasSecondSubNav;
|
||||||
use App\Web\Traits\PageHasServer;
|
use App\Web\Pages\Servers\Page;
|
||||||
use Exception;
|
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
@ -17,17 +14,12 @@
|
|||||||
|
|
||||||
class Index extends Page implements HasSecondSubNav
|
class Index extends Page implements HasSecondSubNav
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
use Traits\Navigation;
|
use Traits\Navigation;
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/databases';
|
protected static ?string $slug = 'servers/{server}/databases';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Databases';
|
protected static ?string $title = 'Databases';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [Database::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [Database::class, static::getServerFromRoute()]) ?? false;
|
||||||
@ -67,7 +59,7 @@ protected function getHeaderActions(): array
|
|||||||
])
|
])
|
||||||
->modalSubmitActionLabel('Create')
|
->modalSubmitActionLabel('Create')
|
||||||
->action(function (array $data) {
|
->action(function (array $data) {
|
||||||
try {
|
run_action($this, function () use ($data) {
|
||||||
app(CreateDatabase::class)->create($this->server, $data);
|
app(CreateDatabase::class)->create($this->server, $data);
|
||||||
|
|
||||||
$this->dispatch('$refresh');
|
$this->dispatch('$refresh');
|
||||||
@ -76,14 +68,7 @@ protected function getHeaderActions(): array
|
|||||||
->success()
|
->success()
|
||||||
->title('Database Created!')
|
->title('Database Created!')
|
||||||
->send();
|
->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\CreateDatabase;
|
||||||
use App\Actions\Database\CreateDatabaseUser;
|
use App\Actions\Database\CreateDatabaseUser;
|
||||||
use App\Models\DatabaseUser;
|
use App\Models\DatabaseUser;
|
||||||
use App\Models\Server;
|
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Contracts\HasSecondSubNav;
|
use App\Web\Contracts\HasSecondSubNav;
|
||||||
use App\Web\Traits\PageHasServer;
|
use App\Web\Pages\Servers\Page;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
@ -18,17 +16,12 @@
|
|||||||
|
|
||||||
class Users extends Page implements HasSecondSubNav
|
class Users extends Page implements HasSecondSubNav
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
use Traits\Navigation;
|
use Traits\Navigation;
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/databases/users';
|
protected static ?string $slug = 'servers/{server}/databases/users';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Database Users';
|
protected static ?string $title = 'Database Users';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [DatabaseUser::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [DatabaseUser::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
use App\Actions\Database\RestoreBackup;
|
use App\Actions\Database\RestoreBackup;
|
||||||
use App\Models\Backup;
|
use App\Models\Backup;
|
||||||
use App\Models\BackupFile;
|
use App\Models\BackupFile;
|
||||||
use Exception;
|
use App\Models\Database;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Support\Enums\MaxWidth;
|
use Filament\Support\Enums\MaxWidth;
|
||||||
@ -63,7 +63,7 @@ public function getTable(): Table
|
|||||||
->icon('heroicon-o-arrow-path')
|
->icon('heroicon-o-arrow-path')
|
||||||
->modalHeading('Restore Backup')
|
->modalHeading('Restore Backup')
|
||||||
->tooltip('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([
|
->form([
|
||||||
Select::make('database')
|
Select::make('database')
|
||||||
->label('Restore to')
|
->label('Restore to')
|
||||||
@ -73,23 +73,23 @@ public function getTable(): Table
|
|||||||
])
|
])
|
||||||
->modalWidth(MaxWidth::Large)
|
->modalWidth(MaxWidth::Large)
|
||||||
->action(function (BackupFile $record, array $data) {
|
->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);
|
app(RestoreBackup::class)->restore($record, $data);
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->success()
|
->success()
|
||||||
->title('Backup is being restored')
|
->title('Backup is being restored')
|
||||||
->send();
|
->send();
|
||||||
} catch (Exception $e) {
|
|
||||||
Notification::make()
|
|
||||||
->danger()
|
|
||||||
->title($e->getMessage())
|
|
||||||
->send();
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->dispatch('$refresh');
|
$this->dispatch('$refresh');
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
Action::make('delete')
|
Action::make('delete')
|
||||||
->hiddenLabel()
|
->hiddenLabel()
|
||||||
@ -100,18 +100,10 @@ public function getTable(): Table
|
|||||||
->authorize(fn (BackupFile $record) => auth()->user()->can('delete', $record))
|
->authorize(fn (BackupFile $record) => auth()->user()->can('delete', $record))
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->action(function (BackupFile $record) {
|
->action(function (BackupFile $record) {
|
||||||
try {
|
run_action($this, function () use ($record) {
|
||||||
$record->delete();
|
$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\Actions\FirewallRule\CreateRule;
|
||||||
use App\Models\FirewallRule;
|
use App\Models\FirewallRule;
|
||||||
use App\Models\Server;
|
use App\Web\Pages\Servers\Page;
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
@ -16,18 +14,12 @@
|
|||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/firewall';
|
protected static ?string $slug = 'servers/{server}/firewall';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Firewall';
|
protected static ?string $title = 'Firewall';
|
||||||
|
|
||||||
protected $listeners = ['$refresh'];
|
protected $listeners = ['$refresh'];
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [FirewallRule::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [FirewallRule::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -2,24 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Web\Pages\Servers\Logs;
|
namespace App\Web\Pages\Servers\Logs;
|
||||||
|
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\ServerLog;
|
use App\Models\ServerLog;
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
|
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
|
||||||
use App\Web\Traits\PageHasServer;
|
use App\Web\Pages\Servers\Page;
|
||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/logs';
|
protected static ?string $slug = 'servers/{server}/logs';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Logs';
|
protected static ?string $title = 'Logs';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [ServerLog::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [ServerLog::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -4,9 +4,13 @@
|
|||||||
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Models\ServerLog;
|
use App\Models\ServerLog;
|
||||||
|
use App\Models\Site;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Tables\Actions\Action;
|
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\Columns\TextColumn;
|
||||||
use Filament\Tables\Filters\Filter;
|
use Filament\Tables\Filters\Filter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@ -18,12 +22,20 @@ class LogsList extends Widget
|
|||||||
{
|
{
|
||||||
public Server $server;
|
public Server $server;
|
||||||
|
|
||||||
|
public ?Site $site = null;
|
||||||
|
|
||||||
|
public ?string $label = '';
|
||||||
|
|
||||||
protected function getTableQuery(): Builder
|
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
|
protected function getTableColumns(): array
|
||||||
{
|
{
|
||||||
@ -68,6 +80,7 @@ public function getTable(): Table
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
->heading($this->label)
|
||||||
->actions([
|
->actions([
|
||||||
Action::make('view')
|
Action::make('view')
|
||||||
->hiddenLabel()
|
->hiddenLabel()
|
||||||
@ -90,6 +103,19 @@ public function getTable(): Table
|
|||||||
->icon('heroicon-o-archive-box-arrow-down')
|
->icon('heroicon-o-archive-box-arrow-down')
|
||||||
->authorize(fn ($record) => auth()->user()->can('view', $record))
|
->authorize(fn ($record) => auth()->user()->can('view', $record))
|
||||||
->action(fn (ServerLog $record) => $record->download()),
|
->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;
|
namespace App\Web\Pages\Servers\Metrics;
|
||||||
|
|
||||||
use App\Models\Metric;
|
use App\Models\Metric;
|
||||||
use App\Models\Server;
|
use App\Web\Pages\Servers\Page;
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/metrics';
|
protected static ?string $slug = 'servers/{server}/metrics';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Metrics';
|
protected static ?string $title = 'Metrics';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [Metric::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [Metric::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -3,27 +3,19 @@
|
|||||||
namespace App\Web\Pages\Servers\PHP;
|
namespace App\Web\Pages\Servers\PHP;
|
||||||
|
|
||||||
use App\Actions\PHP\InstallNewPHP;
|
use App\Actions\PHP\InstallNewPHP;
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\Service;
|
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\Pages\Servers\PHP\Widgets\PHPList;
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\ActionGroup;
|
use Filament\Actions\ActionGroup;
|
||||||
use Filament\Support\Enums\IconPosition;
|
use Filament\Support\Enums\IconPosition;
|
||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/php';
|
protected static ?string $slug = 'servers/{server}/php';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'PHP';
|
protected static ?string $title = 'PHP';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [Service::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [Service::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Web\Traits;
|
namespace App\Web\Pages\Servers;
|
||||||
|
|
||||||
use App\Models\Server;
|
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\Console\Index as ConsoleIndex;
|
||||||
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
|
use App\Web\Pages\Servers\CronJobs\Index as CronJobsIndex;
|
||||||
use App\Web\Pages\Servers\Databases\Index as DatabasesIndex;
|
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\Services\Index as ServicesIndex;
|
||||||
use App\Web\Pages\Servers\Settings as ServerSettings;
|
use App\Web\Pages\Servers\Settings as ServerSettings;
|
||||||
use App\Web\Pages\Servers\Sites\Index as SitesIndex;
|
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\View as ServerView;
|
||||||
use App\Web\Pages\Servers\Widgets\ServerSummary;
|
use App\Web\Pages\Servers\Widgets\ServerSummary;
|
||||||
use Filament\Navigation\NavigationItem;
|
use Filament\Navigation\NavigationItem;
|
||||||
use Illuminate\Support\Facades\Request;
|
use Illuminate\Support\Facades\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
trait PageHasServer
|
abstract class Page extends BasePage
|
||||||
{
|
{
|
||||||
|
public Server $server;
|
||||||
|
|
||||||
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
|
||||||
public function getSubNavigation(): array
|
public function getSubNavigation(): array
|
||||||
{
|
{
|
||||||
$items = [];
|
$items = [];
|
@ -4,10 +4,8 @@
|
|||||||
|
|
||||||
use App\Actions\SshKey\CreateSshKey;
|
use App\Actions\SshKey\CreateSshKey;
|
||||||
use App\Actions\SshKey\DeployKeyToServer;
|
use App\Actions\SshKey\DeployKeyToServer;
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\SshKey;
|
use App\Models\SshKey;
|
||||||
use App\Web\Components\Page;
|
use App\Web\Pages\Servers\Page;
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
@ -18,16 +16,10 @@
|
|||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/ssh-keys';
|
protected static ?string $slug = 'servers/{server}/ssh-keys';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'SSH Keys';
|
protected static ?string $title = 'SSH Keys';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAnyServer', [SshKey::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAnyServer', [SshKey::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -3,10 +3,8 @@
|
|||||||
namespace App\Web\Pages\Servers\Services;
|
namespace App\Web\Pages\Servers\Services;
|
||||||
|
|
||||||
use App\Actions\Service\Install;
|
use App\Actions\Service\Install;
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Web\Components\Page;
|
use App\Web\Pages\Servers\Page;
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
@ -15,16 +13,10 @@
|
|||||||
|
|
||||||
class Index extends Page
|
class Index extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/services';
|
protected static ?string $slug = 'servers/{server}/services';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Services';
|
protected static ?string $title = 'Services';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [Service::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [Service::class, static::getServerFromRoute()]) ?? false;
|
||||||
|
@ -4,18 +4,14 @@
|
|||||||
|
|
||||||
use App\Actions\Server\RebootServer;
|
use App\Actions\Server\RebootServer;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Pages\Servers\Widgets\ServerDetails;
|
use App\Web\Pages\Servers\Widgets\ServerDetails;
|
||||||
use App\Web\Pages\Servers\Widgets\UpdateServerInfo;
|
use App\Web\Pages\Servers\Widgets\UpdateServerInfo;
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions\DeleteAction;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
|
|
||||||
class Settings extends Page
|
class Settings extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}/settings';
|
protected static ?string $slug = 'servers/{server}/settings';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
@ -24,8 +20,6 @@ class Settings extends Page
|
|||||||
|
|
||||||
protected $listeners = ['$refresh'];
|
protected $listeners = ['$refresh'];
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('update', static::getServerFromRoute()) ?? false;
|
return auth()->user()?->can('update', static::getServerFromRoute()) ?? false;
|
||||||
|
@ -2,24 +2,28 @@
|
|||||||
|
|
||||||
namespace App\Web\Pages\Servers\Sites;
|
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\Models\Site;
|
||||||
use App\Web\Components\Page;
|
use App\Models\SourceControl;
|
||||||
use App\Web\Traits\PageHasServer;
|
use App\Web\Pages\Settings\SourceControls\Actions\Create;
|
||||||
use Filament\Actions\CreateAction;
|
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 ?string $slug = 'servers/{server}/sites';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Sites';
|
protected static ?string $title = 'Sites';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('viewAny', [Site::class, static::getServerFromRoute()]) ?? false;
|
return auth()->user()?->can('viewAny', [Site::class, static::getServerFromRoute()]) ?? false;
|
||||||
@ -35,11 +39,107 @@ public function getWidgets(): array
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
CreateAction::make()
|
Action::make('read-the-docs')
|
||||||
->authorize(fn () => auth()->user()?->can('create', [Site::class, $this->server]))
|
->label('Read the Docs')
|
||||||
->createAnother(false)
|
->icon('heroicon-o-document-text')
|
||||||
|
->color('gray')
|
||||||
|
->url('https://vitodeploy.com/sites/create-site.html')
|
||||||
|
->openUrlInNewTab(),
|
||||||
|
Action::make('create')
|
||||||
->label('Create a Site')
|
->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\Server;
|
||||||
use App\Models\Site;
|
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\Actions\Action;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@ -28,11 +28,12 @@ protected function getTableColumns(): array
|
|||||||
TextColumn::make('id')
|
TextColumn::make('id')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('server.name')
|
TextColumn::make('domain')
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('domain')
|
TextColumn::make('created_at')
|
||||||
->searchable(),
|
->formatStateUsing(fn (Site $record) => $record->created_at_by_timezone)
|
||||||
|
->sortable(),
|
||||||
TextColumn::make('status')
|
TextColumn::make('status')
|
||||||
->label('Status')
|
->label('Status')
|
||||||
->badge()
|
->badge()
|
||||||
@ -45,13 +46,13 @@ protected function getTableColumns(): array
|
|||||||
public function getTable(): Table
|
public function getTable(): Table
|
||||||
{
|
{
|
||||||
return $this->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([
|
->actions([
|
||||||
// Action::make('settings')
|
Action::make('settings')
|
||||||
// ->label('Settings')
|
->label('Settings')
|
||||||
// ->icon('heroicon-o-cog-6-tooth')
|
->icon('heroicon-o-cog-6-tooth')
|
||||||
// ->authorize(fn ($record) => auth()->user()->can('update', $record))
|
->authorize(fn (Site $record) => auth()->user()->can('update', [$record, $this->server]))
|
||||||
// ->url(fn (Server $record) => '/'),
|
->url(fn (Site $record) => '/'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,27 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Web\Pages\Servers;
|
namespace App\Web\Pages\Servers;
|
||||||
|
|
||||||
use App\Models\Server;
|
|
||||||
use App\Models\ServerLog;
|
use App\Models\ServerLog;
|
||||||
use App\Web\Components\Page;
|
|
||||||
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
|
use App\Web\Pages\Servers\Logs\Widgets\LogsList;
|
||||||
use App\Web\Pages\Servers\Widgets\Installing;
|
use App\Web\Pages\Servers\Widgets\Installing;
|
||||||
use App\Web\Pages\Servers\Widgets\ServerStats;
|
use App\Web\Pages\Servers\Widgets\ServerStats;
|
||||||
use App\Web\Traits\PageHasServer;
|
|
||||||
use Livewire\Attributes\On;
|
use Livewire\Attributes\On;
|
||||||
|
|
||||||
class View extends Page
|
class View extends Page
|
||||||
{
|
{
|
||||||
use PageHasServer;
|
|
||||||
|
|
||||||
protected static ?string $slug = 'servers/{server}';
|
protected static ?string $slug = 'servers/{server}';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Overview';
|
protected static ?string $title = 'Overview';
|
||||||
|
|
||||||
public Server $server;
|
|
||||||
|
|
||||||
public string $previousStatus;
|
public string $previousStatus;
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
@ -58,7 +49,12 @@ public function getWidgets(): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (auth()->user()->can('viewAny', [ServerLog::class, $this->server])) {
|
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;
|
return $widgets;
|
||||||
|
@ -73,6 +73,7 @@ public function infolist(Infolist $infolist): Infolist
|
|||||||
Action::make('update-server')
|
Action::make('update-server')
|
||||||
->icon('heroicon-o-check-circle')
|
->icon('heroicon-o-check-circle')
|
||||||
->tooltip('Update Now')
|
->tooltip('Update Now')
|
||||||
|
->requiresConfirmation()
|
||||||
->action(function (Server $record) {
|
->action(function (Server $record) {
|
||||||
app(Update::class)->update($record);
|
app(Update::class)->update($record);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use App\Actions\Projects\DeleteProject;
|
use App\Actions\Projects\DeleteProject;
|
||||||
use App\Models\Project;
|
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\AddUser;
|
||||||
use App\Web\Pages\Settings\Projects\Widgets\ProjectUsersList;
|
use App\Web\Pages\Settings\Projects\Widgets\ProjectUsersList;
|
||||||
use App\Web\Pages\Settings\Projects\Widgets\UpdateProject;
|
use App\Web\Pages\Settings\Projects\Widgets\UpdateProject;
|
||||||
@ -19,8 +19,6 @@ class Settings extends Page
|
|||||||
|
|
||||||
protected static ?string $title = 'Project Settings';
|
protected static ?string $title = 'Project Settings';
|
||||||
|
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
return auth()->user()?->can('update', request()->route('project')) ?? false;
|
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 [];
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -1,6 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?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>
|
<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>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -11,13 +11,18 @@ .choices__item--selectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fi-sidebar {
|
.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 {
|
.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 {
|
.fi-btn-color-primary {
|
||||||
background-image: linear-gradient(to bottom right, rgba(var(--primary-500), 1), rgba(var(--primary-900), 1));
|
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;
|
box-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
|
||||||
|
@ -1,40 +1,40 @@
|
|||||||
import ace from 'brace';
|
import ace from "brace";
|
||||||
import 'brace/mode/javascript';
|
import "brace/mode/javascript";
|
||||||
import 'brace/mode/plain_text';
|
import "brace/mode/plain_text";
|
||||||
import 'brace/mode/sh';
|
import "brace/mode/sh";
|
||||||
import 'brace/mode/ini';
|
import "brace/mode/ini";
|
||||||
import 'brace/ext/searchbox'
|
import "brace/ext/searchbox";
|
||||||
import './theme-vito'
|
import "./theme-vito";
|
||||||
import './mode-env';
|
import "./mode-env";
|
||||||
import './mode-nginx';
|
import "./mode-nginx";
|
||||||
|
|
||||||
window.initAceEditor = function (options = {}) {
|
window.initAceEditor = function (options = {}) {
|
||||||
const editorValue = JSON.parse(options.value || '');
|
const editorValue = JSON.parse(options.value || "");
|
||||||
const editor = ace.edit(options.id);
|
const editor = ace.edit(options.id);
|
||||||
editor.setTheme("ace/theme/vito");
|
editor.setTheme("ace/theme/vito");
|
||||||
editor.getSession().setMode(`ace/mode/${options.lang || 'plain_text'}`);
|
|
||||||
editor.setValue(editorValue, -1);
|
editor.setValue(editorValue, -1);
|
||||||
editor.clearSelection();
|
editor.clearSelection();
|
||||||
editor.focus();
|
editor.focus();
|
||||||
editor.setOptions({
|
editor.setOptions({
|
||||||
enableBasicAutocompletion: true,
|
// enableBasicAutocompletion: true,
|
||||||
enableSnippets: true,
|
// enableSnippets: true,
|
||||||
enableLiveAutocompletion: true,
|
// enableLiveAutocompletion: true,
|
||||||
printMargin: false,
|
printMargin: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.renderer.setScrollMargin(15, 15, 0, 0)
|
editor.renderer.setScrollMargin(15, 15, 0, 0);
|
||||||
editor.renderer.setPadding(15);
|
editor.renderer.setPadding(15);
|
||||||
|
|
||||||
editor.getSession().on('change', function () {
|
editor.getSession().on("change", function () {
|
||||||
document.getElementById(`textarea-${options.id}`).value = editor.getValue();
|
document.getElementById(`textarea-${options.id}`).value =
|
||||||
|
editor.getValue();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener("resize", function () {
|
||||||
editor.resize();
|
editor.resize();
|
||||||
})
|
});
|
||||||
|
|
||||||
document.getElementById(`textarea-${options.id}`).innerHTML = editorValue;
|
document.getElementById(`textarea-${options.id}`).innerHTML = editorValue;
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
}
|
};
|
||||||
|
@ -1,62 +1,7 @@
|
|||||||
import 'flowbite';
|
import CodeEditorAlpinePlugin from "./components/editor";
|
||||||
import 'flowbite/dist/datepicker.js';
|
|
||||||
import './ace-editor/ace-editor';
|
|
||||||
|
|
||||||
import Alpine from 'alpinejs';
|
document.addEventListener("alpine:init", () => {
|
||||||
window.Alpine = Alpine;
|
window.Alpine.plugin(CodeEditorAlpinePlugin);
|
||||||
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');
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.copyToClipboard = async function (text) {
|
window.copyToClipboard = async function (text) {
|
||||||
@ -73,11 +18,11 @@ window.copyToClipboard = async function (text) {
|
|||||||
textArea.select();
|
textArea.select();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
document.execCommand('copy');
|
document.execCommand("copy");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//
|
//
|
||||||
} finally {
|
} finally {
|
||||||
textArea.remove();
|
textArea.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
48
resources/js/components/editor.js
Normal file
48
resources/js/components/editor.js
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<form wire:submit="submit">
|
<form>
|
||||||
{{ $this->form }}
|
{{ $this->form }}
|
||||||
</form>
|
</form>
|
||||||
<x-filament-actions::modals />
|
<x-filament-actions::modals />
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
<div {{ $this->getExtraAttributesBag() }}>
|
<div {{ $this->getExtraAttributesBag() }}>
|
||||||
<x-filament-panels::page>
|
<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()" />
|
<x-filament-panels::page.sub-navigation.tabs class="!flex" :navigation="$this->getSecondSubNavigation()" />
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@foreach ($this->getWidgets() as $key => $widget)
|
@foreach ($this->getWidgets() as $key => $widget)
|
||||||
@livewire($widget[0], $widget[1] ?? [], key(class_basename($widget[0]) . "-" . $key))
|
@livewire($widget[0], $widget[1] ?? [], key(class_basename($widget[0]) . "-" . $key))
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
<x-filament-actions::modals />
|
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
</div>
|
</div>
|
||||||
|
18
resources/views/web/fields/code-editor.blade.php
Normal file
18
resources/views/web/fields/code-editor.blade.php
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user