mirror of
https://github.com/vitodeploy/vito.git
synced 2025-04-20 02:11:36 +00:00
2.x - backups
This commit is contained in:
parent
e4fed24498
commit
2e9620409b
@ -7,7 +7,6 @@
|
||||
use App\Models\Backup;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
@ -17,17 +16,15 @@ class CreateBackup
|
||||
* @throws AuthorizationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create($type, Server $server, array $input): Backup
|
||||
public function create(Server $server, array $input): Backup
|
||||
{
|
||||
$this->validate($type, $server, $input);
|
||||
|
||||
$backup = new Backup([
|
||||
'type' => $type,
|
||||
'type' => 'database',
|
||||
'server_id' => $server->id,
|
||||
'database_id' => $input['backup_database'] ?? null,
|
||||
'storage_id' => $input['backup_storage'],
|
||||
'interval' => $input['backup_interval'] == 'custom' ? $input['backup_custom'] : $input['backup_interval'],
|
||||
'keep_backups' => $input['backup_keep'],
|
||||
'database_id' => $input['database'] ?? null,
|
||||
'storage_id' => $input['storage'],
|
||||
'interval' => $input['interval'] == 'custom' ? $input['custom_interval'] : $input['interval'],
|
||||
'keep_backups' => $input['keep'],
|
||||
'status' => BackupStatus::RUNNING,
|
||||
]);
|
||||
$backup->save();
|
||||
@ -37,45 +34,35 @@ public function create($type, Server $server, array $input): Backup
|
||||
return $backup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validate($type, Server $server, array $input): void
|
||||
public static function rules(Server $server, array $input): array
|
||||
{
|
||||
$rules = [
|
||||
'backup_storage' => [
|
||||
'storage' => [
|
||||
'required',
|
||||
Rule::exists('storage_providers', 'id'),
|
||||
],
|
||||
'backup_keep' => [
|
||||
'keep' => [
|
||||
'required',
|
||||
'numeric',
|
||||
'min:1',
|
||||
],
|
||||
'backup_interval' => [
|
||||
'interval' => [
|
||||
'required',
|
||||
Rule::in([
|
||||
'0 * * * *',
|
||||
'0 0 * * *',
|
||||
'0 0 * * 0',
|
||||
'0 0 1 * *',
|
||||
'custom',
|
||||
]),
|
||||
Rule::in(array_keys(config('core.cronjob_intervals'))),
|
||||
],
|
||||
];
|
||||
if ($input['backup_interval'] == 'custom') {
|
||||
$rules['backup_custom'] = [
|
||||
'required',
|
||||
];
|
||||
}
|
||||
if ($type === 'database') {
|
||||
$rules['backup_database'] = [
|
||||
'database' => [
|
||||
'required',
|
||||
Rule::exists('databases', 'id')
|
||||
->where('server_id', $server->id)
|
||||
->where('status', DatabaseStatus::READY),
|
||||
],
|
||||
];
|
||||
if ($input['interval'] == 'custom') {
|
||||
$rules['custom_interval'] = [
|
||||
'required',
|
||||
];
|
||||
}
|
||||
Validator::make($input, $rules)->validate();
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,11 @@
|
||||
use App\Enums\BackupFileStatus;
|
||||
use App\Models\BackupFile;
|
||||
use App\Models\Database;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RestoreBackup
|
||||
{
|
||||
public function restore(BackupFile $backupFile, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
/** @var Database $database */
|
||||
$database = Database::query()->findOrFail($input['database']);
|
||||
$backupFile->status = BackupFileStatus::RESTORING;
|
||||
@ -20,7 +17,9 @@ public function restore(BackupFile $backupFile, array $input): void
|
||||
$backupFile->save();
|
||||
|
||||
dispatch(function () use ($backupFile, $database) {
|
||||
$database->server->database()->handler()->restoreBackup($backupFile, $database->name);
|
||||
/** @var \App\SSH\Services\Database\Database $databaseHandler */
|
||||
$databaseHandler = $database->server->database()->handler();
|
||||
$databaseHandler->restoreBackup($backupFile, $database->name);
|
||||
$backupFile->status = BackupFileStatus::RESTORED;
|
||||
$backupFile->restored_at = now();
|
||||
$backupFile->save();
|
||||
@ -30,10 +29,13 @@ public function restore(BackupFile $backupFile, array $input): void
|
||||
})->onConnection('ssh');
|
||||
}
|
||||
|
||||
private function validate(array $input): void
|
||||
public static function rules(): array
|
||||
{
|
||||
Validator::make($input, [
|
||||
'database' => 'required|exists:databases,id',
|
||||
])->validate();
|
||||
return [
|
||||
'database' => [
|
||||
'required',
|
||||
'exists:databases,id',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,10 @@
|
||||
namespace App\Actions\Database;
|
||||
|
||||
use App\Enums\BackupFileStatus;
|
||||
use App\Enums\BackupStatus;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupFile;
|
||||
use App\SSH\Services\Database\Database;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RunBackup
|
||||
@ -18,11 +20,20 @@ public function run(Backup $backup): BackupFile
|
||||
]);
|
||||
$file->save();
|
||||
|
||||
dispatch(function () use ($file) {
|
||||
$file->backup->server->database()->handler()->runBackup($file);
|
||||
dispatch(function () use ($file, $backup) {
|
||||
/** @var Database $databaseHandler */
|
||||
$databaseHandler = $file->backup->server->database()->handler();
|
||||
$databaseHandler->runBackup($file);
|
||||
$file->status = BackupFileStatus::CREATED;
|
||||
$file->save();
|
||||
})->catch(function () use ($file) {
|
||||
|
||||
if ($backup->status !== BackupStatus::RUNNING) {
|
||||
$backup->status = BackupStatus::RUNNING;
|
||||
$backup->save();
|
||||
}
|
||||
})->catch(function () use ($file, $backup) {
|
||||
$backup->status = BackupStatus::FAILED;
|
||||
$backup->save();
|
||||
$file->status = BackupFileStatus::FAILED;
|
||||
$file->save();
|
||||
})->onConnection('ssh');
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
use App\Models\StorageProvider;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
@ -15,8 +14,6 @@ class CreateStorageProvider
|
||||
*/
|
||||
public function create(User $user, array $input): void
|
||||
{
|
||||
$this->validate($user, $input);
|
||||
|
||||
$storageProvider = new StorageProvider([
|
||||
'user_id' => $user->id,
|
||||
'provider' => $input['provider'],
|
||||
@ -24,8 +21,6 @@ public function create(User $user, array $input): void
|
||||
'project_id' => isset($input['global']) && $input['global'] ? null : $user->current_project_id,
|
||||
]);
|
||||
|
||||
$this->validateProvider($input, $storageProvider->provider()->validationRules());
|
||||
|
||||
$storageProvider->credentials = $storageProvider->provider()->credentialData($input);
|
||||
|
||||
try {
|
||||
@ -43,22 +38,25 @@ public function create(User $user, array $input): void
|
||||
$storageProvider->save();
|
||||
}
|
||||
|
||||
private function validate(User $user, array $input): void
|
||||
public static function rules(array $input): array
|
||||
{
|
||||
Validator::make($input, [
|
||||
$rules = [
|
||||
'provider' => [
|
||||
'required',
|
||||
Rule::in(config('core.storage_providers')),
|
||||
],
|
||||
'name' => [
|
||||
'required',
|
||||
Rule::unique('storage_providers', 'profile')->where('user_id', $user->id),
|
||||
],
|
||||
])->validate();
|
||||
}
|
||||
];
|
||||
|
||||
private function validateProvider(array $input, array $rules): void
|
||||
{
|
||||
Validator::make($input, $rules)->validate();
|
||||
if (isset($input['provider'])) {
|
||||
$provider = (new StorageProvider(['provider' => $input['provider']]))->provider();
|
||||
$rules = array_merge($rules, $provider->validationRules());
|
||||
}
|
||||
|
||||
ds($rules);
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
|
@ -4,15 +4,12 @@
|
||||
|
||||
use App\Models\StorageProvider;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EditStorageProvider
|
||||
{
|
||||
public function edit(StorageProvider $storageProvider, User $user, array $input): void
|
||||
{
|
||||
$this->validate($input);
|
||||
|
||||
$storageProvider->profile = $input['name'];
|
||||
$storageProvider->project_id = isset($input['global']) && $input['global'] ? null : $user->current_project_id;
|
||||
|
||||
@ -22,13 +19,12 @@ public function edit(StorageProvider $storageProvider, User $user, array $input)
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validate(array $input): void
|
||||
public static function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
return [
|
||||
'name' => [
|
||||
'required',
|
||||
],
|
||||
];
|
||||
Validator::make($input, $rules)->validate();
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,7 @@
|
||||
|
||||
final class BackupStatus
|
||||
{
|
||||
const READY = 'ready';
|
||||
|
||||
const RUNNING = 'running';
|
||||
|
||||
const FAILED = 'failed';
|
||||
|
||||
const DELETING = 'deleting';
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
/**
|
||||
* @property string $type
|
||||
@ -46,14 +47,14 @@ public static function boot(): void
|
||||
parent::boot();
|
||||
|
||||
static::deleting(function (Backup $backup) {
|
||||
$backup->files()->delete();
|
||||
$backup->files()->each(function (BackupFile $file) {
|
||||
$file->delete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static array $statusColors = [
|
||||
BackupStatus::READY => 'success',
|
||||
BackupStatus::RUNNING => 'warning',
|
||||
BackupStatus::DELETING => 'warning',
|
||||
BackupStatus::RUNNING => 'success',
|
||||
BackupStatus::FAILED => 'danger',
|
||||
];
|
||||
|
||||
@ -76,4 +77,9 @@ public function files(): HasMany
|
||||
{
|
||||
return $this->hasMany(BackupFile::class, 'backup_id');
|
||||
}
|
||||
|
||||
public function lastFile(): HasOne
|
||||
{
|
||||
return $this->hasOne(BackupFile::class, 'backup_id')->latest();
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,8 @@ protected static function booted(): void
|
||||
BackupFileStatus::CREATING => 'warning',
|
||||
BackupFileStatus::FAILED => 'danger',
|
||||
BackupFileStatus::DELETING => 'warning',
|
||||
BackupFileStatus::RESTORED => 'warning',
|
||||
BackupFileStatus::RESTORING => 'warning',
|
||||
BackupFileStatus::RESTORED => 'primary',
|
||||
BackupFileStatus::RESTORE_FAILED => 'danger',
|
||||
];
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
* @property array $credentials
|
||||
* @property User $user
|
||||
* @property int $project_id
|
||||
* @property string $image_url
|
||||
*/
|
||||
class StorageProvider extends AbstractModel
|
||||
{
|
||||
@ -61,4 +62,9 @@ public static function getByProjectId(int $projectId): Builder
|
||||
->where('project_id', $projectId)
|
||||
->orWhereNull('project_id');
|
||||
}
|
||||
|
||||
public function getImageUrlAttribute(): string
|
||||
{
|
||||
return url('/static/images/'.$this->provider.'.svg');
|
||||
}
|
||||
}
|
||||
|
41
app/Policies/BackupFilePolicy.php
Normal file
41
app/Policies/BackupFilePolicy.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupFile;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class BackupFilePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user, Backup $backup): bool
|
||||
{
|
||||
return ($user->isAdmin() || $backup->server->project->users->contains($user)) && $backup->server->isReady();
|
||||
}
|
||||
|
||||
public function view(User $user, BackupFile $backupFile): bool
|
||||
{
|
||||
return ($user->isAdmin() || $backupFile->backup->server->project->users->contains($user)) &&
|
||||
$backupFile->backup->server->isReady();
|
||||
}
|
||||
|
||||
public function create(User $user, Backup $backup): bool
|
||||
{
|
||||
return ($user->isAdmin() || $backup->server->project->users->contains($user)) && $backup->server->isReady();
|
||||
}
|
||||
|
||||
public function update(User $user, BackupFile $backupFile): bool
|
||||
{
|
||||
return ($user->isAdmin() || $backupFile->backup->server->project->users->contains($user)) &&
|
||||
$backupFile->backup->server->isReady();
|
||||
}
|
||||
|
||||
public function delete(User $user, BackupFile $backupFile): bool
|
||||
{
|
||||
return ($user->isAdmin() || $backupFile->backup->server->project->users->contains($user)) &&
|
||||
$backupFile->backup->server->isReady();
|
||||
}
|
||||
}
|
37
app/Policies/StorageProviderPolicy.php
Normal file
37
app/Policies/StorageProviderPolicy.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\StorageProvider;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class StorageProviderPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function view(User $user, StorageProvider $storageProvider): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function update(User $user, StorageProvider $storageProvider): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
|
||||
public function delete(User $user, StorageProvider $storageProvider): bool
|
||||
{
|
||||
return $user->isAdmin();
|
||||
}
|
||||
}
|
@ -93,6 +93,7 @@ public function panel(Panel $panel): Panel
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
])
|
||||
->login()
|
||||
->spa()
|
||||
->globalSearchKeyBindings(['command+k', 'ctrl+k'])
|
||||
->globalSearchFieldKeyBindingSuffix();
|
||||
|
@ -12,7 +12,12 @@ public function validationRules(): array
|
||||
{
|
||||
return [
|
||||
'host' => 'required',
|
||||
'port' => 'required|numeric',
|
||||
'port' => [
|
||||
'required',
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:65535',
|
||||
],
|
||||
'path' => 'required',
|
||||
'username' => 'required',
|
||||
'password' => 'required',
|
||||
|
@ -13,11 +13,11 @@ class S3 extends S3AbstractStorageProvider
|
||||
public function validationRules(): array
|
||||
{
|
||||
return [
|
||||
'key' => 'required|string',
|
||||
'secret' => 'required|string',
|
||||
'region' => 'required|string',
|
||||
'bucket' => 'required|string',
|
||||
'path' => 'required|string',
|
||||
'key' => 'required',
|
||||
'secret' => 'required',
|
||||
'region' => 'required',
|
||||
'bucket' => 'required',
|
||||
'path' => 'required',
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -15,11 +15,11 @@ class Wasabi extends S3AbstractStorageProvider
|
||||
public function validationRules(): array
|
||||
{
|
||||
return [
|
||||
'key' => 'required|string',
|
||||
'secret' => 'required|string',
|
||||
'region' => 'required|string',
|
||||
'bucket' => 'required|string',
|
||||
'path' => 'required|string',
|
||||
'key' => 'required',
|
||||
'secret' => 'required',
|
||||
'region' => 'required',
|
||||
'bucket' => 'required',
|
||||
'path' => 'required',
|
||||
];
|
||||
}
|
||||
|
||||
|
26
app/Web/Components/Link.php
Normal file
26
app/Web/Components/Link.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Components;
|
||||
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Link extends Component implements Htmlable
|
||||
{
|
||||
public function __construct(public string $href, public string $text, public bool $external = false) {}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('web.components.link');
|
||||
}
|
||||
|
||||
public function toHtml(): View|string
|
||||
{
|
||||
return $this->render()->with([
|
||||
'href' => $this->href,
|
||||
'text' => $this->text,
|
||||
'external' => $this->external,
|
||||
]);
|
||||
}
|
||||
}
|
8
app/Web/Contracts/HasSecondSubNav.php
Normal file
8
app/Web/Contracts/HasSecondSubNav.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Contracts;
|
||||
|
||||
interface HasSecondSubNav
|
||||
{
|
||||
public function getSecondSubNavigation(): array;
|
||||
}
|
@ -2,12 +2,22 @@
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases;
|
||||
|
||||
use App\Actions\Database\CreateBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\Server;
|
||||
use App\Models\StorageProvider;
|
||||
use App\Web\Components\Page;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
use App\Web\Pages\Settings\StorageProviders\Actions\Create;
|
||||
use App\Web\Traits\PageHasServer;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Backups extends Page
|
||||
class Backups extends Page implements HasSecondSubNav
|
||||
{
|
||||
use PageHasServer;
|
||||
use Traits\Navigation;
|
||||
@ -28,7 +38,73 @@ public static function canAccess(): bool
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('Create backup')
|
||||
->icon('heroicon-o-plus')
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->authorize(fn () => auth()->user()?->can('create', [Backup::class, $this->server]))
|
||||
->form([
|
||||
Select::make('database')
|
||||
->label('Database')
|
||||
->options($this->server->databases()->pluck('name', 'id')->toArray())
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['database'])
|
||||
->searchable(),
|
||||
Select::make('storage')
|
||||
->label('Storage')
|
||||
->options(StorageProvider::getByProjectId($this->server->project_id)->pluck('profile', 'id')->toArray())
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['storage'])
|
||||
->suffixAction(
|
||||
\Filament\Forms\Components\Actions\Action::make('connect')
|
||||
->form(Create::form())
|
||||
->modalHeading('Connect to a new storage provider')
|
||||
->modalSubmitActionLabel('Connect')
|
||||
->icon('heroicon-o-wifi')
|
||||
->tooltip('Connect to a new storage provider')
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->authorize(fn () => auth()->user()->can('create', StorageProvider::class))
|
||||
->action(fn (array $data) => Create::action($data))
|
||||
),
|
||||
Select::make('interval')
|
||||
->label('Interval')
|
||||
->options(config('core.cronjob_intervals'))
|
||||
->reactive()
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['interval']),
|
||||
TextInput::make('custom_interval')
|
||||
->label('Custom Interval (Cron)')
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['custom_interval'])
|
||||
->visible(fn (callable $get) => $get('interval') === 'custom')
|
||||
->placeholder('0 * * * *'),
|
||||
TextInput::make('keep')
|
||||
->label('Backups to Keep')
|
||||
->rules(fn (callable $get) => CreateBackup::rules($this->server, $get())['keep'])
|
||||
->helperText('How many backups to keep before deleting the oldest one'),
|
||||
])
|
||||
->modalSubmitActionLabel('Create')
|
||||
->action(function (array $data) {
|
||||
try {
|
||||
app(CreateBackup::class)->create($this->server, $data);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Backup created!')
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\BackupsList::class, ['server' => $this->server]],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
use App\Models\Database;
|
||||
use App\Models\Server;
|
||||
use App\Web\Components\Page;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
use App\Web\Traits\PageHasServer;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
@ -14,7 +15,7 @@
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
class Index extends Page implements HasSecondSubNav
|
||||
{
|
||||
use PageHasServer;
|
||||
use Traits\Navigation;
|
||||
|
@ -10,8 +10,6 @@
|
||||
|
||||
trait Navigation
|
||||
{
|
||||
public bool $hasSecondSubNavigation = true;
|
||||
|
||||
public function getSecondSubNavigation(): array
|
||||
{
|
||||
$items = [];
|
||||
@ -32,7 +30,7 @@ public function getSecondSubNavigation(): array
|
||||
|
||||
if (Backups::canAccess()) {
|
||||
$items[] = NavigationItem::make(Backups::getNavigationLabel())
|
||||
->icon('heroicon-o-circle-stack')
|
||||
->icon('heroicon-o-cloud')
|
||||
->isActiveWhen(fn () => request()->routeIs(Backups::getRouteName()))
|
||||
->url(Backups::getUrl(parameters: ['server' => $this->server]));
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
use App\Models\DatabaseUser;
|
||||
use App\Models\Server;
|
||||
use App\Web\Components\Page;
|
||||
use App\Web\Contracts\HasSecondSubNav;
|
||||
use App\Web\Traits\PageHasServer;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
@ -15,7 +16,7 @@
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Users extends Page
|
||||
class Users extends Page implements HasSecondSubNav
|
||||
{
|
||||
use PageHasServer;
|
||||
use Traits\Navigation;
|
||||
|
118
app/Web/Pages/Servers/Databases/Widgets/BackupFilesList.php
Normal file
118
app/Web/Pages/Servers/Databases/Widgets/BackupFilesList.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Widgets;
|
||||
|
||||
use App\Actions\Database\RestoreBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupFile;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class BackupFilesList extends Widget
|
||||
{
|
||||
public Backup $backup;
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return BackupFile::query()->where('backup_id', $this->backup->id);
|
||||
}
|
||||
|
||||
protected static ?string $heading = '';
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('name')
|
||||
->searchable(),
|
||||
TextColumn::make('created_at')
|
||||
->formatStateUsing(fn (BackupFile $record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
TextColumn::make('restored_to')
|
||||
->searchable(),
|
||||
TextColumn::make('restored_at')
|
||||
->formatStateUsing(fn (BackupFile $record) => $record->getDateTimeByTimezone($record->restored_at))
|
||||
->sortable(),
|
||||
TextColumn::make('status')
|
||||
->badge()
|
||||
->color(fn ($state) => BackupFile::$statusColors[$state])
|
||||
->sortable(),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
protected function applyDefaultSortingToTableQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->latest('created_at');
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table
|
||||
->actions([
|
||||
Action::make('restore')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->modalHeading('Restore Backup')
|
||||
->tooltip('Restore Backup')
|
||||
->authorize(fn (BackupFile $record) => auth()->user()->can('update', $record->backup->database))
|
||||
->form([
|
||||
Select::make('database')
|
||||
->label('Restore to')
|
||||
->options($this->backup->server->databases()->pluck('name', 'id'))
|
||||
->rules(RestoreBackup::rules()['database'])
|
||||
->native(false),
|
||||
])
|
||||
->modalWidth(MaxWidth::Large)
|
||||
->action(function (BackupFile $record, array $data) {
|
||||
try {
|
||||
app(RestoreBackup::class)->restore($record, $data);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Backup is being restored')
|
||||
->send();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
Action::make('delete')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-trash')
|
||||
->modalHeading('Delete Database')
|
||||
->color('danger')
|
||||
->tooltip('Delete')
|
||||
->authorize(fn (BackupFile $record) => auth()->user()->can('delete', $record))
|
||||
->requiresConfirmation()
|
||||
->action(function (BackupFile $record) {
|
||||
try {
|
||||
$record->delete();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
113
app/Web/Pages/Servers/Databases/Widgets/BackupsList.php
Normal file
113
app/Web/Pages/Servers/Databases/Widgets/BackupsList.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Servers\Databases\Widgets;
|
||||
|
||||
use App\Actions\Database\RunBackup;
|
||||
use App\Models\Backup;
|
||||
use App\Models\BackupFile;
|
||||
use App\Models\Server;
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Widgets\TableWidget as Widget;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class BackupsList extends Widget
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected $listeners = ['$refresh'];
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Backup::query()->where('server_id', $this->server->id);
|
||||
}
|
||||
|
||||
protected static ?string $heading = '';
|
||||
|
||||
protected function getTableColumns(): array
|
||||
{
|
||||
return [
|
||||
TextColumn::make('database.name')
|
||||
->label('Database')
|
||||
->searchable(),
|
||||
TextColumn::make('storage.profile')
|
||||
->label('Storage')
|
||||
->searchable(),
|
||||
TextColumn::make('status')
|
||||
->label('Status')
|
||||
->badge()
|
||||
->color(fn (Backup $backup) => Backup::$statusColors[$backup->status])
|
||||
->sortable(),
|
||||
TextColumn::make('lastFile.status')
|
||||
->label('Last file status')
|
||||
->badge()
|
||||
->color(fn ($state) => BackupFile::$statusColors[$state])
|
||||
->sortable(),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone)
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table
|
||||
->actions([
|
||||
Action::make('files')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-rectangle-stack')
|
||||
->modalHeading('Backup Files')
|
||||
->color('secondary')
|
||||
->tooltip('Show backup files')
|
||||
->authorize(fn (Backup $record) => auth()->user()->can('viewAny', [BackupFile::class, $record]))
|
||||
->modalContent(fn (Backup $record) => view('web.components.dynamic-widget', [
|
||||
'widget' => BackupFilesList::class,
|
||||
'params' => [
|
||||
'backup' => $record,
|
||||
],
|
||||
]))
|
||||
->modalWidth(MaxWidth::FiveExtraLarge)
|
||||
->slideOver()
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Close')
|
||||
->modalFooterActions([
|
||||
Action::make('backup')
|
||||
->label('Run Backup')
|
||||
->icon('heroicon-o-play')
|
||||
->color('primary')
|
||||
->action(function (Backup $record) {
|
||||
app(RunBackup::class)->run($record);
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]),
|
||||
Action::make('delete')
|
||||
->hiddenLabel()
|
||||
->icon('heroicon-o-trash')
|
||||
->modalHeading('Delete Database')
|
||||
->color('danger')
|
||||
->tooltip('Delete')
|
||||
->authorize(fn (Backup $record) => auth()->user()->can('delete', $record))
|
||||
->requiresConfirmation()
|
||||
->action(function (Backup $record) {
|
||||
try {
|
||||
$record->delete();
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title($e->getMessage())
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -2,9 +2,21 @@
|
||||
|
||||
namespace App\Web\Pages\Servers;
|
||||
|
||||
use App\Actions\Server\CreateServer as CreateServerAction;
|
||||
use App\Enums\ServerProvider;
|
||||
use App\Enums\ServerType;
|
||||
use App\Models\Server;
|
||||
use App\Web\Components\Page;
|
||||
use Filament\Actions\Action;
|
||||
use App\Web\Fields\AlertField;
|
||||
use App\Web\Fields\ProviderField;
|
||||
use App\Web\Pages\Settings\ServerProviders\Actions\Create;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Throwable;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
@ -35,12 +47,194 @@ public function getWidgets(): array
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$publicKey = __('servers.create.public_key_text', [
|
||||
'public_key' => get_public_key_content(),
|
||||
]);
|
||||
|
||||
return [
|
||||
Action::make('create')
|
||||
\Filament\Actions\Action::make('create')
|
||||
->label('Create a Server')
|
||||
->icon('heroicon-o-plus')
|
||||
->url(Create::getUrl())
|
||||
->authorize('create', Server::class),
|
||||
->authorize('create', Server::class)
|
||||
->modalWidth(MaxWidth::FiveExtraLarge)
|
||||
->slideOver()
|
||||
->form([
|
||||
ProviderField::make('provider')
|
||||
->label('Select a provider')
|
||||
->default(ServerProvider::CUSTOM)
|
||||
->live()
|
||||
->reactive()
|
||||
->afterStateUpdated(function (callable $set) {
|
||||
$set('server_provider', null);
|
||||
$set('region', null);
|
||||
$set('plan', null);
|
||||
})
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['provider']),
|
||||
AlertField::make('alert')
|
||||
->warning()
|
||||
->message(__('servers.create.public_key_warning'))
|
||||
->visible(fn ($get) => $get('provider') === ServerProvider::CUSTOM),
|
||||
Select::make('server_provider')
|
||||
->visible(fn ($get) => $get('provider') !== ServerProvider::CUSTOM)
|
||||
->label('Server provider connection')
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['server_provider'])
|
||||
->options(function ($get) {
|
||||
return \App\Models\ServerProvider::getByProjectId(auth()->user()->current_project_id)
|
||||
->where('provider', $get('provider'))
|
||||
->pluck('profile', 'id');
|
||||
})
|
||||
->suffixAction(
|
||||
Action::make('connect')
|
||||
->form(Create::form())
|
||||
->modalHeading('Connect to a new server provider')
|
||||
->modalSubmitActionLabel('Connect')
|
||||
->icon('heroicon-o-wifi')
|
||||
->tooltip('Connect to a new server provider')
|
||||
->modalWidth(MaxWidth::Medium)
|
||||
->authorize(fn () => auth()->user()->can('create', \App\Models\ServerProvider::class))
|
||||
->action(fn (array $data) => Create::action($data))
|
||||
)
|
||||
->placeholder('Select profile')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->visible(fn ($get) => $get('provider') !== ServerProvider::CUSTOM),
|
||||
Grid::make()
|
||||
->schema([
|
||||
Select::make('region')
|
||||
->label('Region')
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['region'])
|
||||
->live()
|
||||
->reactive()
|
||||
->options(function ($get) {
|
||||
if (! $get('server_provider')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return \App\Models\ServerProvider::regions($get('serer_provider'));
|
||||
})
|
||||
->loadingMessage('Loading regions...')
|
||||
->disabled(fn ($get) => ! $get('server_provider'))
|
||||
->placeholder(fn ($get) => $get('server_provider') ? 'Select region' : 'Select connection first')
|
||||
->searchable(),
|
||||
Select::make('plan')
|
||||
->label('Plan')
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['plan'])
|
||||
->reactive()
|
||||
->options(function ($get) {
|
||||
if (! $get('server_provider') || ! $get('region')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return \App\Models\ServerProvider::plans($get('server_provider'), $get('region'));
|
||||
})
|
||||
->loadingMessage('Loading plans...')
|
||||
->disabled(fn ($get) => ! $get('region'))
|
||||
->placeholder(fn ($get) => $get('region') ? 'Select plan' : 'Select plan first')
|
||||
->searchable(),
|
||||
])
|
||||
->visible(fn ($get) => $get('provider') !== ServerProvider::CUSTOM),
|
||||
TextInput::make('public_key')
|
||||
->label('Public Key')
|
||||
->default($publicKey)
|
||||
->suffixAction(
|
||||
Action::make('copy')
|
||||
->icon('heroicon-o-clipboard-document-list')
|
||||
->tooltip('Copy')
|
||||
->action(function ($livewire, $state) {
|
||||
$livewire->js(
|
||||
'window.navigator.clipboard.writeText("'.$state.'");'
|
||||
);
|
||||
Notification::make()
|
||||
->success()
|
||||
->title('Copied!')
|
||||
->send();
|
||||
})
|
||||
)
|
||||
->helperText('Run this command on your server as root user')
|
||||
->disabled()
|
||||
->visible(fn ($get) => $get('provider') === ServerProvider::CUSTOM),
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['name']),
|
||||
Grid::make()
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
->label('SSH IP Address')
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['ip']),
|
||||
TextInput::make('port')
|
||||
->label('SSH Port')
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['port']),
|
||||
])
|
||||
->visible(fn ($get) => $get('provider') === ServerProvider::CUSTOM),
|
||||
Grid::make()
|
||||
->schema([
|
||||
Select::make('os')
|
||||
->label('OS')
|
||||
->native(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['os'])
|
||||
->options(
|
||||
collect(config('core.operating_systems'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
Select::make('type')
|
||||
->label('Server Type')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['type'])
|
||||
->options(
|
||||
collect(config('core.server_types'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
)
|
||||
->default(ServerType::REGULAR),
|
||||
]),
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
Select::make('webserver')
|
||||
->label('Webserver')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['webserver'] ?? [])
|
||||
->options(
|
||||
collect(config('core.webservers'))->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
Select::make('database')
|
||||
->label('Database')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['database'] ?? [])
|
||||
->options(
|
||||
collect(config('core.databases_name'))
|
||||
->mapWithKeys(fn ($value, $key) => [
|
||||
$key => $value.' '.config('core.databases_version')[$key],
|
||||
])
|
||||
),
|
||||
Select::make('php')
|
||||
->label('PHP')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($get())['php'] ?? [])
|
||||
->options(
|
||||
collect(config('core.php_versions'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
]),
|
||||
])
|
||||
->action(function ($input) {
|
||||
$this->authorize('create', Server::class);
|
||||
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
$server = app(CreateServerAction::class)->create(auth()->user(), $input);
|
||||
|
||||
$this->redirect(View::getUrl(['server' => $server]));
|
||||
} catch (Throwable $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
use App\Models\Server;
|
||||
use App\Web\Fields\AlertField;
|
||||
use App\Web\Fields\ProviderField;
|
||||
use App\Web\Pages\Servers\Index;
|
||||
use App\Web\Pages\Servers\View;
|
||||
use App\Web\Pages\Settings\ServerProviders\Actions\Create;
|
||||
use Filament\Forms\Components\Actions;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
@ -73,7 +73,6 @@ public function form(Form $form): Form
|
||||
->default(ServerProvider::CUSTOM)
|
||||
->live()
|
||||
->reactive()
|
||||
->reactive()
|
||||
->afterStateUpdated(function (callable $set) {
|
||||
$set('server_provider', null);
|
||||
$set('region', null);
|
||||
@ -184,21 +183,20 @@ public function form(Form $form): Form
|
||||
Select::make('os')
|
||||
->label('OS')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['os'])
|
||||
->options(function () {
|
||||
return collect(config('core.operating_systems'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value]);
|
||||
}),
|
||||
->options(
|
||||
collect(config('core.operating_systems'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
Select::make('type')
|
||||
->label('Server Type')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['type'])
|
||||
->options(function () {
|
||||
return collect(config('core.server_types'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value]);
|
||||
})
|
||||
->options(
|
||||
collect(config('core.server_types'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
)
|
||||
->default(ServerType::REGULAR),
|
||||
]),
|
||||
Grid::make(3)
|
||||
@ -208,30 +206,29 @@ public function form(Form $form): Form
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['webserver'] ?? [])
|
||||
->options(function () {
|
||||
return collect(config('core.webservers'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value]);
|
||||
}),
|
||||
->options(
|
||||
collect(config('core.webservers'))->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
Select::make('database')
|
||||
->label('Database')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['database'] ?? [])
|
||||
->options(function () {
|
||||
return collect(config('core.databases_name'))
|
||||
->options(
|
||||
collect(config('core.databases_name'))
|
||||
->mapWithKeys(fn ($value, $key) => [
|
||||
$key => $value.' '.config('core.databases_version')[$key],
|
||||
]);
|
||||
}),
|
||||
])
|
||||
),
|
||||
Select::make('php')
|
||||
->label('PHP')
|
||||
->native(false)
|
||||
->selectablePlaceholder(false)
|
||||
->rules(fn ($get) => CreateServerAction::rules($this->all())['php'] ?? [])
|
||||
->options(function () {
|
||||
return collect(config('core.php_versions'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value]);
|
||||
}),
|
||||
->options(
|
||||
collect(config('core.php_versions'))
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
),
|
||||
]),
|
||||
Actions::make([
|
||||
Action::make('create')
|
||||
@ -252,7 +249,7 @@ public function submit(): void
|
||||
try {
|
||||
$server = app(CreateServerAction::class)->create(auth()->user(), $this->all()['data']);
|
||||
|
||||
$this->redirect(Index::getUrl());
|
||||
$this->redirect(View::getUrl(['server' => $server]));
|
||||
} catch (Throwable $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
|
@ -54,7 +54,17 @@ public function infolist(Infolist $infolist): Infolist
|
||||
Action::make('check-update')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->tooltip('Check Now')
|
||||
->action(fn (Server $record) => $record->checkForUpdates())
|
||||
->action(function (Server $record) {
|
||||
$record->checkForUpdates();
|
||||
|
||||
$this->dispatch('$refresh');
|
||||
|
||||
Notification::make()
|
||||
->info()
|
||||
->title('Available updates:')
|
||||
->body($record->available_updates)
|
||||
->send();
|
||||
})
|
||||
),
|
||||
TextEntry::make('available_updates')
|
||||
->label('Available Updates')
|
||||
|
139
app/Web/Pages/Settings/StorageProviders/Actions/Create.php
Normal file
139
app/Web/Pages/Settings/StorageProviders/Actions/Create.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\StorageProviders\Actions;
|
||||
|
||||
use App\Actions\StorageProvider\CreateStorageProvider;
|
||||
use App\Enums\StorageProvider;
|
||||
use App\Web\Components\Link;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class Create
|
||||
{
|
||||
public static function form(): array
|
||||
{
|
||||
return [
|
||||
Select::make('provider')
|
||||
->options(
|
||||
collect(config('core.storage_providers'))
|
||||
->mapWithKeys(fn ($provider) => [$provider => $provider])
|
||||
)
|
||||
->live()
|
||||
->reactive()
|
||||
->native(false)
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['provider']),
|
||||
TextInput::make('name')
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['name']),
|
||||
TextInput::make('token')
|
||||
->label('API Token')
|
||||
->validationAttribute('API Token')
|
||||
->visible(fn ($get) => $get('provider') == StorageProvider::DROPBOX)
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['token']),
|
||||
Grid::make()
|
||||
->visible(fn ($get) => $get('provider') == StorageProvider::FTP)
|
||||
->schema([
|
||||
TextInput::make('host')
|
||||
->visible(fn ($get) => $get('provider') == StorageProvider::FTP)
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['host']),
|
||||
TextInput::make('port')
|
||||
->visible(fn ($get) => $get('provider') == StorageProvider::FTP)
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['port']),
|
||||
TextInput::make('username')
|
||||
->visible(fn ($get) => $get('provider') == StorageProvider::FTP)
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['username']),
|
||||
TextInput::make('password')
|
||||
->visible(fn ($get) => $get('provider') == StorageProvider::FTP)
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['password']),
|
||||
Checkbox::make('ssl')
|
||||
->visible(fn ($get) => $get('provider') == StorageProvider::FTP)
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['ssl']),
|
||||
Checkbox::make('passive')
|
||||
->visible(fn ($get) => $get('provider') == StorageProvider::FTP)
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['passive']),
|
||||
]),
|
||||
TextInput::make('path')
|
||||
->visible(fn ($get) => in_array($get('provider'), [
|
||||
StorageProvider::S3,
|
||||
StorageProvider::WASABI,
|
||||
StorageProvider::FTP,
|
||||
StorageProvider::LOCAL,
|
||||
]))
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['path'])
|
||||
->helperText(function ($get) {
|
||||
return match ($get('provider')) {
|
||||
StorageProvider::LOCAL => 'The absolute path on your server that the database exists. like `/home/vito/db-backups`',
|
||||
default => '',
|
||||
};
|
||||
}),
|
||||
Grid::make()
|
||||
->visible(fn ($get) => in_array($get('provider'), [
|
||||
StorageProvider::S3,
|
||||
StorageProvider::WASABI,
|
||||
]))
|
||||
->schema([
|
||||
TextInput::make('key')
|
||||
->visible(fn ($get) => in_array($get('provider'), [
|
||||
StorageProvider::S3,
|
||||
StorageProvider::WASABI,
|
||||
]))
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['key'])
|
||||
->helperText(function ($get) {
|
||||
return match ($get('provider')) {
|
||||
StorageProvider::S3 => new Link(
|
||||
href: 'https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html',
|
||||
text: 'How to generate?',
|
||||
external: true
|
||||
),
|
||||
StorageProvider::WASABI => new Link(
|
||||
href: 'https://docs.wasabi.com/docs/creating-a-user-account-and-access-key',
|
||||
text: 'How to generate?',
|
||||
external: true
|
||||
),
|
||||
default => '',
|
||||
};
|
||||
}),
|
||||
TextInput::make('secret')
|
||||
->visible(fn ($get) => in_array($get('provider'), [
|
||||
StorageProvider::S3,
|
||||
StorageProvider::WASABI,
|
||||
]))
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['secret']),
|
||||
TextInput::make('region')
|
||||
->visible(fn ($get) => in_array($get('provider'), [
|
||||
StorageProvider::S3,
|
||||
StorageProvider::WASABI,
|
||||
]))
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['region']),
|
||||
TextInput::make('bucket')
|
||||
->visible(fn ($get) => in_array($get('provider'), [
|
||||
StorageProvider::S3,
|
||||
StorageProvider::WASABI,
|
||||
]))
|
||||
->rules(fn ($get) => CreateStorageProvider::rules($get())['bucket']),
|
||||
]),
|
||||
Checkbox::make('global')
|
||||
->label('Is Global (Accessible in all projects)'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function action(array $data): void
|
||||
{
|
||||
try {
|
||||
app(CreateStorageProvider::class)->create(auth()->user(), $data);
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title($e->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
27
app/Web/Pages/Settings/StorageProviders/Actions/Edit.php
Normal file
27
app/Web/Pages/Settings/StorageProviders/Actions/Edit.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\StorageProviders\Actions;
|
||||
|
||||
use App\Actions\StorageProvider\EditStorageProvider;
|
||||
use App\Models\StorageProvider;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
|
||||
class Edit
|
||||
{
|
||||
public static function form(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('name')
|
||||
->label('Name')
|
||||
->rules(EditStorageProvider::rules()['name']),
|
||||
Checkbox::make('global')
|
||||
->label('Is Global (Accessible in all projects)'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function action(StorageProvider $provider, array $data): void
|
||||
{
|
||||
app(EditStorageProvider::class)->edit($provider, auth()->user(), $data);
|
||||
}
|
||||
}
|
49
app/Web/Pages/Settings/StorageProviders/Index.php
Normal file
49
app/Web/Pages/Settings/StorageProviders/Index.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\StorageProviders;
|
||||
|
||||
use App\Enums\StorageProvider;
|
||||
use App\Web\Components\Page;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
protected static ?string $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?string $slug = 'settings/storage-providers';
|
||||
|
||||
protected static ?string $title = 'Storage Providers';
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-circle-stack';
|
||||
|
||||
protected static ?int $navigationSort = 6;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()?->can('viewAny', StorageProvider::class) ?? false;
|
||||
}
|
||||
|
||||
public function getWidgets(): array
|
||||
{
|
||||
return [
|
||||
[Widgets\StorageProvidersList::class],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Connect')
|
||||
->icon('heroicon-o-wifi')
|
||||
->modalHeading('Connect to a Storage Provider')
|
||||
->modalSubmitActionLabel('Connect')
|
||||
->createAnother(false)
|
||||
->form(Actions\Create::form())
|
||||
->authorize('create', StorageProvider::class)
|
||||
->modalWidth(MaxWidth::ExtraLarge)
|
||||
->using(fn (array $data) => Actions\Create::action($data)),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Web\Pages\Settings\StorageProviders\Widgets;
|
||||
|
||||
use App\Actions\StorageProvider\DeleteStorageProvider;
|
||||
use App\Models\StorageProvider;
|
||||
use App\Web\Pages\Settings\StorageProviders\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 StorageProvidersList extends Widget
|
||||
{
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return StorageProvider::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 ($record) => $record->profile)
|
||||
->label('Name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('id')
|
||||
->label('Global')
|
||||
->badge()
|
||||
->color(fn ($record) => $record->project_id ? 'gray' : 'success')
|
||||
->formatStateUsing(function (StorageProvider $record) {
|
||||
return $record->project_id ? 'No' : 'Yes';
|
||||
}),
|
||||
TextColumn::make('created_at')
|
||||
->label('Created At')
|
||||
->formatStateUsing(fn ($record) => $record->created_at_by_timezone)
|
||||
->searchable()
|
||||
->sortable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getTable(): Table
|
||||
{
|
||||
return $this->table->actions([
|
||||
EditAction::make('edit')
|
||||
->label('Edit')
|
||||
->modalHeading('Edit Storage Provider')
|
||||
->mutateRecordDataUsing(function (array $data, StorageProvider $record) {
|
||||
return [
|
||||
'name' => $record->profile,
|
||||
'global' => $record->project_id === null,
|
||||
];
|
||||
})
|
||||
->form(Edit::form())
|
||||
->authorize(fn (StorageProvider $record) => auth()->user()->can('update', $record))
|
||||
->using(fn (array $data, StorageProvider $record) => Edit::action($record, $data))
|
||||
->modalWidth(MaxWidth::Medium),
|
||||
DeleteAction::make('delete')
|
||||
->label('Delete')
|
||||
->modalHeading('Delete Storage Provider')
|
||||
->authorize(fn (StorageProvider $record) => auth()->user()->can('delete', $record))
|
||||
->using(function (array $data, StorageProvider $record) {
|
||||
app(DeleteStorageProvider::class)->delete($record);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
@ -481,4 +481,13 @@
|
||||
\App\Enums\UserRole::USER,
|
||||
\App\Enums\UserRole::ADMIN,
|
||||
],
|
||||
|
||||
'cronjob_intervals' => [
|
||||
'* * * * *' => 'Every Minute',
|
||||
'0 * * * *' => 'Hourly',
|
||||
'0 0 * * *' => 'Daily',
|
||||
'0 0 * * 0' => 'Weekly',
|
||||
'0 0 1 * *' => 'Monthly',
|
||||
'custom' => 'Custom',
|
||||
],
|
||||
];
|
||||
|
@ -9,3 +9,8 @@ .fi-breadcrumbs .fi-breadcrumbs-item-label {
|
||||
.choices__item--selectable {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.fi-btn-color-primary {
|
||||
background-image: linear-gradient(to bottom right, rgba(var(--primary-500), 1), rgba(var(--primary-900), 1));
|
||||
box-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 2.2 KiB |
1
resources/views/web/components/dynamic-widget.blade.php
Normal file
1
resources/views/web/components/dynamic-widget.blade.php
Normal file
@ -0,0 +1 @@
|
||||
@livewire($widget, $params ?? [], key($widget))
|
1
resources/views/web/components/link.blade.php
Normal file
1
resources/views/web/components/link.blade.php
Normal file
@ -0,0 +1 @@
|
||||
<a href="{{ $href }}" @if($external) target="_blank" @endif>{{ $text }}</a>
|
Loading…
x
Reference in New Issue
Block a user